DEV Community

Cover image for Building NailArt AI: A Complete Technical Breakdown
Devanshu Goyal
Devanshu Goyal

Posted on

Building NailArt AI: A Complete Technical Breakdown

How I Built an AI-Powered Virtual Nail Art Try-On Platform with 600+ Designs

In this deep dive, I'll walk you through the entire architecture, tech stack decisions, and implementation details of NailArt AI — a platform that lets users try on nail art designs virtually using AI.


Table of Contents

  1. Introduction
  2. The Problem We're Solving
  3. Tech Stack & Why We Chose Each Technology
  4. Architecture Overview
  5. AI Integration: Google Gemini
  6. Database Design: Supabase PostgreSQL
  7. Storage Architecture: Dual-Layer Strategy
  8. Performance Optimizations
  9. SEO Implementation
  10. Virtual Try-On Feature
  11. Admin Dashboard & Content Management
  12. Challenges We Faced & Solutions
  13. What's Next

Introduction

NailArt AI is a comprehensive AI-powered platform that combines image generation, virtual try-on capabilities, and an extensive gallery system. The app allows users to:

  • Generate unique nail art designs using AI (Google Gemini)
  • Try on designs virtually by uploading hand photos
  • Browse 600+ curated designs across 185+ categories
  • Get detailed tutorials with AI-generated editorial content
  • Discover designs by colors, techniques, occasions, seasons, and styles

The platform is built with a focus on performance, SEO, and user experience, handling everything from AI image generation to complex filtering and search functionality.


The Problem We're Solving

Before diving into the technical details, let's understand what we're building:

  1. AI Image Generation: Users need instant, high-quality nail art designs generated from text prompts
  2. Virtual Try-On: Users want to see how designs look on their actual hands before going to a salon
  3. Content Discovery: Users need to find relevant designs through intuitive filtering (by color, occasion, technique, etc.)
  4. SEO & Content: Each design needs rich, SEO-optimized content to rank in search engines
  5. Performance: Fast loading times, especially on mobile devices
  6. Scalability: Handle growing content library efficiently

Tech Stack & Why We Chose Each Technology

Frontend: Next.js 15 + React 19 + Tailwind CSS 4

Why Next.js 15?

  • App Router: Server Components for better SEO and performance
  • Built-in Image Optimization: Automatic image optimization with next/image
  • API Routes: Backend functionality without separate server
  • Server-Side Rendering: Critical for SEO with 1,600+ dynamic pages
  • Edge Runtime: Deploy functions globally for low latency

Why React 19?

  • Latest features and performance improvements
  • Better server component support
  • Improved hydration

Why Tailwind CSS 4?

  • Utility-first approach speeds up development
  • Responsive design out of the box
  • Easy theming (we use a custom pink/white palette)
  • Small bundle size with tree-shaking

Backend: Next.js API Routes + Server Components

All backend logic lives in Next.js API routes (/api/*), making deployment simple on Vercel. We use:

  • Server Components: For SEO-optimized pages (no JavaScript sent to client)
  • API Routes: For dynamic operations (AI generation, image uploads, etc.)
  • Edge Functions: For global performance

Database: Supabase PostgreSQL

Why Supabase?

  • PostgreSQL: Robust, relational database perfect for our structured data
  • Row Level Security (RLS): Built-in security policies
  • Real-time Capabilities: For future features like live updates
  • Auto-generated APIs: REST and GraphQL APIs out of the box
  • Built-in Auth: User authentication ready to use
  • Free Tier: Generous free tier for MVP

Our Schema:

-- Main gallery items table
CREATE TABLE gallery_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  image_url TEXT NOT NULL,
  prompt TEXT NOT NULL,
  design_name TEXT,
  category TEXT,
  original_image_url TEXT,
  colors TEXT[],           -- Array of color tags
  techniques TEXT[],       -- Array of technique tags
  occasions TEXT[],        -- Array of occasion tags
  seasons TEXT[],          -- Array of season tags
  styles TEXT[],           -- Array of style tags
  shapes TEXT[],           -- Array of shape tags
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  user_id UUID REFERENCES auth.users(id)
);

-- AI-generated editorial content
CREATE TABLE gallery_editorials (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  gallery_item_id UUID REFERENCES gallery_items(id),
  title TEXT NOT NULL,
  intro TEXT,
  primary_keyword TEXT,
  secondary_keywords TEXT[],
  supplies TEXT[],
  steps TEXT[],
  faqs JSONB,
  -- ... 20+ more fields for rich content
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Storage: Cloudflare R2 + Supabase Storage

Why Dual Storage?

Supabase Storage (Primary)

  • Used for transactional operations
  • User uploads (hand photos for try-on)
  • Temporary files
  • Integration with database

Cloudflare R2 (CDN Layer)

  • Global CDN for fast image delivery
  • Cost-effective ($0.015/GB storage, $0 egress)
  • Better performance than Supabase for public assets
  • Automatic sync from Supabase to R2

Our Flow:

User Upload → Supabase Storage → R2 Sync → CDN Delivery
Enter fullscreen mode Exit fullscreen mode

AI: Google Gemini API

Why Gemini?

  • Multimodal: Handles both image and text generation
  • Image Generation: gemini-2.5-flash-image-preview for creating nail art
  • Image-to-Image: gemini-2.0-flash-exp for virtual try-on
  • Content Generation: gemini-2.5-pro for SEO-optimized editorial content
  • Cost-Effective: Good pricing compared to alternatives
  • Fast: Low latency for real-time features

Models We Use:

  • gemini-2.5-flash-image-preview: Generating nail art from text prompts
  • gemini-2.0-flash-exp: Applying designs to hand photos (try-on)
  • gemini-2.5-pro: Generating editorial content (tutorials, FAQs, etc.)

Deployment: Vercel

Why Vercel?

  • Zero Config: Works perfectly with Next.js
  • Edge Network: Deploy API routes globally
  • Automatic Deployments: CI/CD built-in
  • Analytics: Built-in performance monitoring
  • Free Tier: Great for MVP

Architecture Overview

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                         Client (Browser)                      │
│  Next.js 15 App Router + React 19 + Tailwind CSS 4          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Vercel Edge Network                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │ API Routes  │  │Server Comp.  │  │ Edge Func.   │    │
│  └──────────────┘  └──────────────┘  └──────────────┘    │
└─────────────────────────────────────────────────────────────┘
          │                    │                    │
          ▼                    ▼                    ▼
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   Gemini    │      │   Supabase  │      │   Cloudflare│
│     AI     │      │  PostgreSQL │      │      R2     │
└─────────────┘      └─────────────┘      └─────────────┘
Enter fullscreen mode Exit fullscreen mode

Folder Structure

nail-art-nextjs/
├── src/
│   ├── app/                    # Next.js App Router pages
│   │   ├── page.tsx            # Homepage
│   │   ├── [category]/[slug]/  # Dynamic design pages
│   │   ├── try-on/             # Virtual try-on interface
│   │   ├── nail-art-gallery/   # Gallery listing
│   │   ├── api/                # API routes (24 endpoints)
│   │   └── admin/               # Admin dashboard
│   ├── components/             # 47 React components
│   │   ├── OptimizedImage.tsx  # Image optimization component
│   │   ├── DesignGalleryGrid.tsx
│   │   ├── CollapsibleSection.tsx
│   │   └── ... (43 more)
│   └── lib/                    # 34 utility libraries
│       ├── geminiService.ts    # AI integration
│       ├── supabase.ts         # Database client
│       ├── r2Service.ts        # Storage service
│       ├── galleryService.ts   # Business logic
│       └── ... (30 more)
├── public/                     # Static assets
└── supabase/
    └── migrations/             # Database migrations
Enter fullscreen mode Exit fullscreen mode

AI Integration: Google Gemini

Image Generation Flow

1. Text-to-Image (Design Generation)

// src/lib/nailArtGenerator.ts
export async function generateNailArtImage(prompt: string): Promise<string | null> {
  // API key stored securely in environment variables - never in code
const aiInstance = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  const enhancedPrompt = `Create a high-quality nail art design: ${prompt}. 
    The image should show:
    - Clean, well-manicured nails
    - Professional nail art application
    - High resolution and detailed work
    - Beautiful lighting and composition
    - Focus on the nail design as the main subject`;

  const response = await aiInstance.models.generateContent({
    model: 'gemini-2.5-flash-image-preview',
    contents: {
      parts: [{ text: enhancedPrompt }],
    },
    config: {
      responseModalities: [Modality.IMAGE, Modality.TEXT],
    },
  });

  // Extract base64 image from response
  const imageData = extractImageFromResponse(response);

  // Upload to R2 CDN
  const imageUrl = await uploadToR2(imageData);

  return imageUrl;
}
Enter fullscreen mode Exit fullscreen mode

2. Image-to-Image (Virtual Try-On)

// src/app/api/generate-nail-art/route.ts
export async function POST(request: NextRequest) {
  const { base64ImageData, mimeType, prompt } = await request.json();

  // Use Gemini's image-to-image capability
  const response = await ai.models.generateContent({
    model: 'gemini-2.0-flash-exp',
    contents: {
      parts: [
        {
          inlineData: {
            data: base64ImageData,
            mimeType: mimeType,
          },
        },
        {
          text: `Apply this nail art design to the nails in the image: ${prompt}. Only change the nails.`,
        },
      ],
    },
    config: {
      responseModalities: [Modality.IMAGE, Modality.TEXT],
    },
  });

  return NextResponse.json({ imageData: extractImageFromResponse(response) });
}
Enter fullscreen mode Exit fullscreen mode

3. Content Generation (Editorial Content)

// src/lib/geminiService.ts
export async function generateEditorialContentForNailArt(
  designName?: string,
  category?: string,
  prompt?: string
): Promise<NailArtEditorial> {
  const sys = `You are a professional nail artist and SEO copywriter...`;

  const response = await aiInstance.models.generateContent({
    model: 'gemini-2.5-pro',
    contents: {
      parts: [
        { text: sys },
        { text: `Design: ${designName}\nCategory: ${category}\nPrompt: ${prompt}` },
      ],
    },
  });

  // Parse JSON response
  return JSON.parse(extractTextFromResponse(response));
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting

We implement strict rate limiting for AI operations since they're expensive. Rate limits are configurable via environment variables and follow industry best practices:

// src/lib/rateLimiter.ts
export const rateLimiters = {
  aiGeneration: {
    maxRequests: parseInt(process.env.AI_RATE_LIMIT || '10'), // Configurable
    windowMs: parseInt(process.env.AI_RATE_WINDOW || '60000'), // 1 minute default
  },
  tryOn: {
    maxRequests: parseInt(process.env.TRYON_RATE_LIMIT || '5'), // Configurable
    windowMs: parseInt(process.env.TRYON_RATE_WINDOW || '60000'),
  },
};
Enter fullscreen mode Exit fullscreen mode

Note: Actual rate limit values are stored in environment variables and adjusted based on server capacity and usage patterns.


Database Design: Supabase PostgreSQL

Key Design Decisions

1. Array Columns for Tags

Instead of separate junction tables, we use PostgreSQL arrays:

colors TEXT[],
techniques TEXT[],
occasions TEXT[],
-- etc.
Enter fullscreen mode Exit fullscreen mode

Why?

  • Simpler queries: WHERE 'red' = ANY(colors)
  • Better performance for read-heavy workloads
  • Easier to maintain
  • PostgreSQL arrays are indexed with GIN indexes

2. Separate Editorial Table

Editorial content is in a separate table to:

  • Keep gallery_items lightweight
  • Allow lazy loading of rich content
  • Enable independent updates
  • Better caching strategies

3. Indexes for Performance

-- Search performance
CREATE INDEX idx_gallery_items_category ON gallery_items(category);
CREATE INDEX idx_gallery_items_design_name ON gallery_items(design_name);
CREATE INDEX idx_gallery_items_created_at ON gallery_items(created_at DESC);

-- Array search (GIN indexes)
CREATE INDEX idx_gallery_items_colors ON gallery_items USING GIN(colors);
CREATE INDEX idx_gallery_items_techniques ON gallery_items USING GIN(techniques);
-- etc.
Enter fullscreen mode Exit fullscreen mode

Query Examples

Get items by category:

const { data } = await supabase
  .from('gallery_items')
  .select('*')
  .eq('category', category)
  .order('created_at', { ascending: false })
  .limit(12);
Enter fullscreen mode Exit fullscreen mode

Get items by color tag:

const { data } = await supabase
  .from('gallery_items')
  .select('*')
  .contains('colors', ['red'])  // PostgreSQL array contains
  .limit(12);
Enter fullscreen mode Exit fullscreen mode

Get items with editorial content:

const { data } = await supabase
  .from('gallery_items')
  .select(`
    *,
    gallery_editorials (*)
  `)
  .eq('id', itemId)
  .single();
Enter fullscreen mode Exit fullscreen mode

Storage Architecture: Dual-Layer Strategy

Why Two Storage Systems?

Supabase Storage:

  • Transactional operations
  • User uploads (hand photos)
  • Database integration
  • Row Level Security

Cloudflare R2:

  • Global CDN delivery
  • Better performance (lower latency)
  • Lower costs (no egress fees)
  • Static asset caching

Implementation

// src/lib/r2Service.ts
export async function uploadToR2(
  file: Buffer,
  key: string,
  contentType: string = 'image/jpeg',
  metadata?: Record<string, string>
): Promise<string> {
  const command = new PutObjectCommand({
    Bucket: process.env.R2_BUCKET_NAME, // Stored in environment variables
    Key: `images/${key}`,
    Body: file,
    ContentType: contentType,
    Metadata: {
      ...metadata,
      'license': 'CC0-1.0',
      'source': 'nailartai.app',
    },
    CacheControl: 'public, max-age=31536000, immutable',
  });

  await r2Client.send(command);
  return `${process.env.R2_PUBLIC_URL}/images/${key}`; // CDN URL from env vars
}
Enter fullscreen mode Exit fullscreen mode

Automatic Sync

When a new image is uploaded to Supabase, we automatically sync it to R2:

// src/app/api/sync-r2/route.ts
export async function POST(request: NextRequest) {
  const { supabaseUrl, itemId } = await request.json();

  // Download from Supabase
  const imageBuffer = await downloadFromSupabase(supabaseUrl);

  // Upload to R2
  const r2Url = await uploadToR2(imageBuffer, generateKey(itemId));

  // Update database with R2 URL
  await supabase
    .from('gallery_items')
    .update({ image_url: r2Url })
    .eq('id', itemId);

  return NextResponse.json({ success: true, url: r2Url });
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimizations

1. Image Optimization

Responsive Images:

// src/components/OptimizedImage.tsx
const IMAGE_PRESETS = {
  thumbnail: { width: 200, height: 200 },
  card: { width: 200, height: 112 },
  detail: { width: 400, height: 600 },
  mobile: { width: 150, height: 150 },
};

export default function OptimizedImage({ src, preset, priority }) {
  const { isMobile } = useMobileOptimization();
  const optimizedSrc = getOptimizedImageUrl(src, isMobile);

  return (
    <img
      src={optimizedSrc}
      loading={priority ? 'eager' : 'lazy'}
      fetchPriority={priority ? 'high' : 'low'}
      decoding={priority ? 'sync' : 'async'}
      sizes={preset === 'mobile' ? '100vw' : '50vw'}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

CDN Optimization:

  • All images served from Cloudflare R2 CDN
  • 1-year cache headers for static assets
  • Mobile-specific image sizes

2. Code Splitting

// Dynamic imports for non-critical components
const CategoryShowcase = dynamic(() => import('@/components/CategoryShowcase'), {
  loading: () => <Skeleton />,
  ssr: false, // Only load on client
});
Enter fullscreen mode Exit fullscreen mode

3. Database Query Optimization

Caching:

// src/lib/galleryCacheService.ts
const cache = new Map<string, { data: any; expires: number }>();

export async function getCachedGalleryItems(options) {
  const key = JSON.stringify(options);
  const cached = cache.get(key);

  if (cached && cached.expires > Date.now()) {
    return cached.data;
  }

  const data = await fetchGalleryItems(options);
  cache.set(key, { data, expires: Date.now() + 60000 }); // 1 min cache
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Optimized Queries:

// Only select needed fields
const { data } = await supabase
  .from('gallery_items')
  .select('id, image_url, design_name, category')  // Specific fields only
  .limit(12);
Enter fullscreen mode Exit fullscreen mode

4. Server Components

Most pages are Server Components for better SEO and performance:

// src/app/[category]/[slug]/page.tsx
export default async function DesignDetailPage({ params }) {
  // This runs on the server - no JS sent to client
  const item = await getGalleryItemBySlug(params.slug);
  const editorial = await getEditorial(item.id);

  return (
    <div>
      {/* Fully rendered HTML sent to client */}
      <DesignHero item={item} />
      <EditorialContent editorial={editorial} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Critical CSS Inlining

// src/app/layout.tsx
<style dangerouslySetInnerHTML={{
  __html: `
    html{height:100%;font-size:16px}
    body{min-height:100%;background:#f8f6f7;color:#1b0d14;margin:0}
    /* Critical styles only */
  `
}} />
Enter fullscreen mode Exit fullscreen mode

SEO Implementation: A Comprehensive Strategy

With 1,600+ pages to optimize, SEO isn't just a nice-to-have—it's essential for discoverability. Our SEO strategy covers technical SEO, on-page optimization, structured data, and content distribution.

1. Dynamic Metadata Generation

Every page has unique, SEO-optimized metadata generated dynamically:

// src/app/[category]/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const item = await getGalleryItemBySlug(params.slug);
  const editorial = await getEditorial(item.id);

  return {
    title: `${editorial.title} | Nail Art AI`,
    description: editorial.intro,
    keywords: editorial.secondaryKeywords,
    openGraph: {
      title: editorial.title,
      description: editorial.intro,
      images: [{ url: item.image_url }],
    },
    twitter: {
      card: 'summary_large_image',
      title: editorial.title,
      images: [item.image_url],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

2. Structured Data (JSON-LD)

// Design detail page includes HowTo and FAQ schema
const structuredData = {
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": editorial.title,
  "description": editorial.intro,
  "step": editorial.steps.map((step, i) => ({
    "@type": "HowToStep",
    "position": i + 1,
    "text": step,
  })),
  "totalTime": `PT${editorial.timeMinutes}M`,
  "tool": editorial.supplies,
};

const faqSchema = {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": editorial.faqs.map(faq => ({
    "@type": "Question",
    "name": faq.q,
    "acceptedAnswer": {
      "@type": "Answer",
      "text": faq.a,
    },
  })),
};
Enter fullscreen mode Exit fullscreen mode

3. Sitemaps

We generate multiple sitemaps for better indexing:

// src/app/sitemap.xml/route.ts
export async function GET() {
  // Generate sitemap index
  return `<?xml version="1.0" encoding="UTF-8"?>
    <sitemapindex>
      <sitemap><loc>https://nailartai.app/sitemap-designs.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-categories.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-static.xml</loc></sitemap>
    </sitemapindex>`;
}
Enter fullscreen mode Exit fullscreen mode

4. Image SEO

// Optimized image metadata
<img
  src={item.image_url}
  alt={generateImageAltText(item.design_name, item.category, item.prompt)}
  // ... other attributes
/>

// Generated alt text
function generateImageAltText(designName, category, prompt) {
  return `${designName || 'Nail Art Design'} - ${category || 'Nail Art'} | ${prompt || 'AI Generated Design'}`;
}
Enter fullscreen mode Exit fullscreen mode

5. Advanced Sitemap Strategy

The Challenge: With 600+ designs across 185+ categories, we need intelligent sitemap management to ensure all content gets indexed efficiently.

Our Solution: Multiple Specialized Sitemaps

Instead of one massive sitemap, we use a sitemap index with specialized sitemaps:

// src/app/sitemap-index.xml/route.ts
export async function GET() {
  return `<?xml version="1.0" encoding="UTF-8"?>
    <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <sitemap><loc>https://nailartai.app/sitemap-static.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-designs.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-categories.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-images.xml</loc></sitemap>
      <sitemap><loc>https://nailartai.app/sitemap-gallery.xml</loc></sitemap>
    </sitemapindex>`;
}
Enter fullscreen mode Exit fullscreen mode

Sitemap Types:

  1. Static Sitemap (sitemap-static.xml): Core pages (homepage, gallery, categories index)
  2. Designs Sitemap (sitemap-designs.xml): All individual design pages (highest priority: 0.8)
  3. Categories Sitemap (sitemap-categories.xml): Category filtering pages, color pages, technique pages
  4. Images Sitemap (sitemap-images.xml): Image metadata for Google Images
  5. Gallery Sitemap (sitemap-gallery.xml): Gallery overview pages

Why This Structure?

  • Prevents Duplicates: Only canonical URLs in each sitemap
  • Priority Management: Important pages get higher priority scores
  • Search Engine Optimization: Different engines can focus on relevant content
  • Pagination Support: Handles 1,000+ items per sitemap automatically

Sitemap Priority System:

function getCategoryPriority(category?: string): number {
  // High-value categories get higher priority
  const highPriorityCategories = [
    'Christmas Nail Art', 'Halloween Nail Art', 
    'Wedding Nail Art', 'French Nail Art'
  ];

  if (category && highPriorityCategories.includes(category)) {
    return 0.9; // Highest priority
  }

  return 0.8; // Standard design pages
}
Enter fullscreen mode Exit fullscreen mode

Automatic Sitemap Updates:

When new content is created, we automatically update sitemaps:

// src/app/api/regenerate-sitemap/route.ts
export async function POST(request: NextRequest) {
  const { newContent } = await request.json();

  // Revalidate relevant sitemaps
  revalidatePath('/sitemap.xml');
  revalidatePath('/sitemap-designs.xml');

  // Ping search engines
  await Promise.all([
    fetch(`https://www.google.com/ping?sitemap=https://nailartai.app/sitemap-index.xml`),
    fetch(`https://www.bing.com/ping?sitemap=https://nailartai.app/sitemap-index.xml`)
  ]);

  return NextResponse.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

6. IndexNow Implementation: Instant Indexing

The Challenge: Waiting for search engines to discover new content can take weeks. We needed instant indexing for new designs.

Our Solution: IndexNow Protocol

IndexNow is an open protocol that allows websites to notify search engines (Bing, Yandex, etc.) immediately when content changes.

Implementation:

// src/lib/indexnowService.ts
export async function submitToIndexNow(urls: string[]): Promise<boolean> {
  const request = {
    host: 'nailartai.app',
    key: process.env.INDEXNOW_API_KEY, // Stored securely in environment variables
    keyLocation: `https://nailartai.app/${process.env.INDEXNOW_API_KEY}.txt`, // Key file location
    urlList: urls
  };

  // Submit to multiple IndexNow-compatible search engines
  const endpoints = [
    'https://api.indexnow.org/indexnow',
    'https://www.bing.com/indexnow',
    'https://yandex.com/indexnow'
  ];

  const promises = endpoints.map(endpoint =>
    fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request)
    })
  );

  await Promise.all(promises);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Usage:

// When a new design is created
async function createNewDesign(item: GalleryItem) {
  // ... save to database ...

  // Submit to IndexNow for instant indexing
  await submitToIndexNow([
    `https://nailartai.app/${category}/${slug}`,
    `https://nailartai.app/design/${slug}`
  ]);
}
Enter fullscreen mode Exit fullscreen mode

Results:

  • New content indexed within hours instead of weeks
  • Better crawl efficiency
  • Improved search visibility for fresh content

7. Category Pages SEO Strategy

The Challenge: Category pages need to rank for broad terms like "Christmas Nail Art" while also helping users discover relevant designs.

Our Strategy:

1. Dynamic Metadata Per Category:

// src/app/nail-art-gallery/category/[category]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const category = decodeURIComponent(params.category);

  return {
    title: `${category} Nail Art Designs | Nail Art AI`,
    description: `Browse our collection of ${category} nail art designs. Discover stunning AI-generated nail art in the ${category} category.`,
    keywords: [
      category.toLowerCase(),
      `${category} nail art`,
      `${category} nail design`,
      `${category} manicure`,
      'nail art',
      'AI nail art'
    ],
    openGraph: {
      title: `${category} Nail Art Designs`,
      description: `Discover beautiful ${category} nail art designs...`,
      images: [getCategoryThumbnail(category)],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

2. Structured Data for Category Pages:

const categorySchema = {
  "@context": "https://schema.org",
  "@type": "CollectionPage",
  "name": `${category} Nail Art Designs`,
  "description": `Browse our collection of ${category} nail art...`,
  "mainEntity": {
    "@type": "ItemList",
    "itemListElement": items.map((item, index) => ({
      "@type": "ListItem",
      "position": index + 1,
      "item": {
        "@type": "CreativeWork",
        "name": item.design_name,
        "url": generateItemUrl(item),
      }
    }))
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Internal Linking Strategy:

Category pages link to:

  • Related categories (color-based, occasion-based)
  • Popular designs within the category
  • Related techniques and styles
  • Parent category (if applicable)

4. Content Distribution:

Each category page includes:

  • Category description with keywords
  • Popular designs showcase
  • Related tags and filters
  • Breadcrumb navigation
  • Related categories section

8. Individual Design Page SEO

The Challenge: Each of 600+ design pages needs unique, SEO-optimized content that ranks for specific long-tail keywords.

Our Multi-Layered Approach:

1. AI-Generated Editorial Content:

Every design gets comprehensive editorial content:

// src/lib/geminiService.ts
export async function generateEditorialContentForNailArt(
  designName?: string,
  category?: string,
  prompt?: string
): Promise<NailArtEditorial> {
  const sys = `You are a professional nail artist and SEO copywriter. Create UNIQUE, DESIGN-SPECIFIC content...`;

  const response = await ai.models.generateContent({
    model: 'gemini-2.5-pro',
    contents: {
      parts: [
        { text: sys },
        { text: `Design: ${designName}\nCategory: ${category}\nPrompt: ${prompt}` }
      ],
    },
  });

  // Returns comprehensive content:
  // - SEO-optimized title (50-60 chars)
  // - Meta description (150-160 chars)
  // - Primary keyword + secondary keywords
  // - Quick facts, supplies, steps
  // - FAQs (5-6 Q&A pairs)
  // - Internal links
  // - And 15+ more fields
}
Enter fullscreen mode Exit fullscreen mode

2. Keyword Optimization:

// Extract and optimize keywords from design data
function generateKeywords(item: GalleryItem, editorial: Editorial): string[] {
  const baseKeywords = ['nail art', 'AI nail art', 'nail design', 'manicure'];

  const categoryKeywords = item.category ? [
    `${item.category} nail art`,
    `${item.category} nail design`
  ] : [];

  const colorKeywords = (item.colors || []).map(color => 
    `${color} nail art`
  );

  const techniqueKeywords = (item.techniques || []).map(technique =>
    `${technique} nail art`
  );

  return [
    ...baseKeywords,
    ...categoryKeywords,
    ...colorKeywords,
    ...techniqueKeywords,
    ...(editorial.secondaryKeywords || [])
  ];
}
Enter fullscreen mode Exit fullscreen mode

3. Multiple Structured Data Types:

Each design page includes multiple schema types:

  • CreativeWork: Main content type
  • HowTo: Step-by-step tutorial
  • FAQPage: Q&A section
  • BreadcrumbList: Navigation hierarchy
  • ImageObject: Image metadata

4. Internal Linking:

Design pages link to:

  • Related designs in same category
  • Related designs with same colors/techniques
  • Category page
  • Technique pages
  • Color pages
  • Occasion pages

9. Pinterest Automation & RSS Feeds

Why Pinterest Matters:

  • Pinterest is a visual search engine (450M+ users)
  • Pins drive traffic for months (unlike social media)
  • Perfect platform for nail art content
  • 85% of users plan purchases on Pinterest

Our Pinterest Strategy:

1. Pinterest Rich Pins Setup

Rich Pins show additional information (author, category, description) making pins more engaging.

// src/lib/pinterestRichPinsService.ts
export function generatePinterestRichPinMetaTags(data: PinterestRichPinData) {
  return {
    'og:type': 'article',
    'og:title': data.title,
    'og:description': data.description,
    'og:image': data.imageUrl,
    'article:author': data.author,
    'article:published_time': data.publishedTime,
    'article:section': data.section,
    'article:tag': data.tags.join(',')
  };
}
Enter fullscreen mode Exit fullscreen mode

2. Pinterest RSS Feed for Automation

We generate an RSS feed optimized for Pinterest automation tools:

// src/app/api/pinterest/rss/route.ts
export async function GET(request: NextRequest) {
  const items = await getGalleryItems({ limit: 50 });

  const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0" xmlns:pinterest="https://www.pinterest.com/xmlns/">
      <channel>
        <title>Nail Art AI - Latest Designs</title>
        ${items.map(item => `
          <item>
            <title><![CDATA[${item.design_name} - ${item.category} Nail Art]]></title>
            <description><![CDATA[Try this ${item.category} nail art design virtually...]]></description>
            <link>https://nailartai.app/${category}/${slug}</link>
            <enclosure url="${item.image_url}" type="image/jpeg"/>
            <pinterest:image>${item.image_url}</pinterest:image>
            <pinterest:description>${optimizedDescription}</pinterest:description>
          </item>
        `).join('')}
      </channel>
    </rss>`;

  return new NextResponse(rssXml, {
    headers: { 'Content-Type': 'application/rss+xml' }
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Automation Integration

Option A: IFTTT/Zapier

Connect RSS feed to Pinterest:

  • Trigger: New RSS item
  • Action: Create Pinterest pin
  • Automatic pinning of new designs

Option B: Pinterest API (Future)

Direct API integration for programmatic pinning:

  • Bulk pin creation
  • Board management
  • Analytics integration

4. Pinterest Optimization

Each pin includes:

  • Optimized Title: Under 100 characters, keyword-rich
  • Compelling Description: Under 500 characters, includes hashtags
  • High-Quality Image: 1000x1500 pixels (2:3 ratio)
  • Relevant Hashtags: 5-10 hashtags per pin
  • Clear CTA: "Try this design virtually!"

RSS Feed URL: https://nailartai.app/api/pinterest/rss

Features:

  • Automatically includes latest 50 designs
  • Filterable by category (?category=christmas)
  • Sortable (?sortBy=newest)
  • Includes all Pinterest Rich Pin metadata
  • Optimized for automation tools

10. Marketing SEO Strategies

Beyond Technical SEO: Content Distribution

1. Content Clusters:

We organize content into topical clusters:

Main Category: "Christmas Nail Art" (primary keyword)
├── Related Designs: "Red Christmas Nail Art"
├── Related Techniques: "Gel Polish Christmas Nail Art"
├── Related Occasions: "Holiday Party Nail Art"
└── Related Colors: "Red and Gold Nail Art"
Enter fullscreen mode Exit fullscreen mode

Each cluster internally links to strengthen topical authority.

2. Long-Tail Keyword Targeting:

We target specific, low-competition keywords:

  • "Christmas nail art with red and gold"
  • "French manicure for wedding"
  • "Halloween nail art easy tutorial"
  • "Summer nail art with glitter"

3. User Intent Optimization:

Pages are optimized for different user intents:

  • Informational: "How to create Christmas nail art"
  • Navigational: "Christmas nail art designs"
  • Transactional: "Try Christmas nail art virtually"

4. Content Freshness:

  • New designs added weekly
  • Editorial content updated quarterly
  • Category pages refreshed monthly
  • Sitemaps updated automatically

5. Social Signals:

  • Pinterest pins drive traffic
  • Social sharing buttons on every page
  • Optimized Open Graph tags
  • Twitter Card metadata

6. Local SEO (Future):

For location-based searches:

  • "Nail art near me"
  • "Nail salon with virtual try-on"
  • City-specific category pages

11. SEO Performance Monitoring

Key Metrics We Track:

  1. Indexing Status:

    • Number of pages indexed
    • Indexing rate (new pages)
    • Coverage issues
  2. Rankings:

    • Keyword rankings for primary terms
    • Category page rankings
    • Design page rankings
  3. Traffic Sources:

    • Organic search traffic
    • Pinterest referrals
    • Direct traffic
    • Social media
  4. Technical SEO:

    • Core Web Vitals scores
    • Mobile usability
    • Page speed
    • Structured data validation

Tools We Use:

  • Google Search Console: Indexing, coverage, performance
  • Google Analytics: Traffic, behavior, conversions
  • Pinterest Analytics: Pin performance, clicks
  • Bing Webmaster Tools: Additional search engine visibility
  • IndexNow Dashboard: Instant indexing status

12. SEO Automation

Auto-SEO Service:

When new content is created, we automatically:

  1. Generate SEO metadata
  2. Create structured data
  3. Update sitemaps
  4. Submit to IndexNow
  5. Generate internal links
  6. Create Pinterest Rich Pin data
// src/app/api/auto-seo/route.ts
export async function POST(request: NextRequest) {
  const { content, pageType } = await request.json();

  // Generate complete SEO package
  const seoData = await autoGenerateSEOForNewContent(content, pageType);

  // Update sitemap
  await updateSitemapForNewContent(content);

  // Submit to IndexNow
  await submitToIndexNow([seoData.canonicalUrl]);

  // Generate Pinterest data
  await generatePinterestRichPinData(content);

  return NextResponse.json({ success: true, seoData });
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Zero manual SEO work
  • Consistent SEO quality
  • Instant indexing
  • Comprehensive optimization


Virtual Try-On Feature

User Flow

  1. User selects a design from gallery
  2. User uploads hand photo
  3. AI applies design to nails
  4. User sees result with comparison slider

Implementation

// src/app/try-on/TryOnContent.tsx
export default function TryOnContent() {
  const [sourceImage, setSourceImage] = useState<string | null>(null);
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
  const [selectedDesign, setSelectedDesign] = useState<GalleryItem | null>(null);

  const handleGenerate = async () => {
    if (!sourceImage || !selectedDesign) return;

    setIsLoading(true);

    try {
      // Convert image to base64
      const base64Data = await imageToBase64(sourceImage);

      // Call AI API
      const response = await fetch('/api/generate-nail-art', {
        method: 'POST',
        body: JSON.stringify({
          base64ImageData: base64Data,
          mimeType: 'image/jpeg',
          prompt: selectedDesign.prompt,
        }),
      });

      const { imageData } = await response.json();
      setGeneratedImage(`data:image/jpeg;base64,${imageData}`);
    } catch (error) {
      setError('Failed to generate image');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <StepIndicator currentStep={currentStep} />
      {currentStep === 1 && <DesignGalleryGrid onSelect={setSelectedDesign} />}
      {currentStep === 2 && <EnhancedUploadArea onUpload={setSourceImage} />}
      {currentStep === 3 && (
        <DraggableComparisonSlider
          before={sourceImage}
          after={generatedImage}
        />
      )}
      <StickyGenerateButton
        onGenerate={handleGenerate}
        disabled={!sourceImage || !selectedDesign}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image Processing

// Convert file to base64
function imageToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const result = reader.result as string;
      resolve(result.split(',')[1]); // Remove data URL prefix
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}
Enter fullscreen mode Exit fullscreen mode

Admin Dashboard & Content Management

Bulk Content Generation

We built scripts to generate hundreds of designs automatically:

// scripts/generate-bulk-nail-art.js
const PROMPT_CATEGORIES = [
  { name: 'Christmas Nail Art', prompts: [...], priority: 1 },
  { name: 'Valentine Nail Art', prompts: [...], priority: 2 },
  // ... 185+ categories
];

async function generateForCategory(category, count) {
  for (const prompt of category.prompts.slice(0, count)) {
    // Generate image
    const imageData = await generateNailArtImage(prompt);

    // Upload to storage
    const imageUrl = await uploadToR2(imageData);

    // Save to database
    await supabase.from('gallery_items').insert({
      image_url: imageUrl,
      prompt: prompt,
      design_name: extractDesignName(prompt),
      category: category.name,
      colors: extractColors(prompt),
      techniques: extractTechniques(prompt),
      // ... more tags
    });

    // Generate editorial content (async, non-blocking)
    generateEditorialAsync(item.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Editorial Content Generation

// Generate rich content for each design
async function generateEditorialForItem(item: GalleryItem) {
  const editorial = await generateEditorialContentForNailArt(
    item.design_name,
    item.category,
    item.prompt
  );

  await supabase.from('gallery_editorials').insert({
    gallery_item_id: item.id,
    title: editorial.title,
    intro: editorial.intro,
    primary_keyword: editorial.primaryKeyword,
    secondary_keywords: editorial.secondaryKeywords,
    supplies: editorial.supplies,
    steps: editorial.steps,
    faqs: editorial.faqs,
    // ... 20+ more fields
  });
}
Enter fullscreen mode Exit fullscreen mode

Challenges We Faced & Solutions

1. AI API Costs

Challenge: Generating images and content for 600+ designs is expensive.

Solution:

  • Rate limiting to prevent abuse
  • Batch processing during off-peak hours
  • Caching generated content
  • Using cheaper models (flash) when possible, pro only for editorial

2. Image Storage Costs

Challenge: Storing thousands of high-resolution images.

Solution:

  • Dual storage: Supabase for transactional, R2 for CDN
  • Image compression (60-65% quality)
  • Responsive image sizes
  • CDN caching (1-year TTL)

3. SEO for 1,600+ Pages

Challenge: Each design needs unique, SEO-optimized content.

Solution:

  • AI-generated editorial content for each design
  • Dynamic metadata generation
  • Structured data (JSON-LD)
  • Multiple sitemaps
  • Image alt text generation

4. Performance on Mobile

Challenge: Large images and complex layouts slow on mobile.

Solution:

  • Mobile-specific image sizes
  • Lazy loading
  • Code splitting
  • Critical CSS inlining
  • Service worker for caching

5. Database Query Performance

Challenge: Complex queries with arrays and joins.

Solution:

  • GIN indexes on array columns
  • Query optimization (select only needed fields)
  • Caching layer
  • Pagination

What's Next

Planned Features

  1. User Accounts: Save favorite designs, personal galleries
  2. Social Sharing: Share designs on Pinterest, Instagram
  3. Advanced Filters: Filter by difficulty, time, cost
  4. AR Try-On: Real-time AR using camera
  5. Design Customization: Edit colors, add patterns
  6. Community: User-submitted designs, ratings
  7. Salon Integration: Book appointments directly

Technical Improvements

  1. GraphQL API: More flexible queries
  2. Real-time Updates: WebSocket for live gallery updates
  3. Advanced Caching: Redis for query caching
  4. Image Processing: On-the-fly image transformations
  5. Analytics: User behavior tracking
  6. A/B Testing: Test different UI variations

Key Takeaways

  1. Choose the right tools: Next.js + Supabase + Cloudflare R2 + Gemini = perfect stack for this use case
  2. Optimize early: Performance optimizations should be considered from day one
  3. SEO matters: With 1,600+ pages, proper SEO implementation is crucial
  4. AI is expensive: Rate limiting and caching are essential
  5. Mobile first: Most users will be on mobile, optimize accordingly
  6. Iterate fast: Use AI to generate content, focus on user experience

Conclusion

Building NailArt AI has been an incredible learning experience. Combining modern web technologies with cutting-edge AI capabilities has allowed us to create something truly unique. The platform is now live at nailartai.app with 600+ designs and growing.

If you're building something similar, I hope this breakdown helps! Feel free to reach out with questions or check out the code on GitHub.


Tech Stack Summary:

  • Frontend: Next.js 15, React 19, Tailwind CSS 4
  • Backend: Next.js API Routes, Server Components
  • Database: Supabase PostgreSQL
  • Storage: Supabase Storage + Cloudflare R2
  • AI: Google Gemini API
  • Deployment: Vercel
  • CDN: Cloudflare R2

Links:


Questions? Comments? Let me know in the comments below!

Top comments (0)