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
- Introduction
- The Problem We're Solving
- Tech Stack & Why We Chose Each Technology
- Architecture Overview
- AI Integration: Google Gemini
- Database Design: Supabase PostgreSQL
- Storage Architecture: Dual-Layer Strategy
- Performance Optimizations
- SEO Implementation
- Virtual Try-On Feature
- Admin Dashboard & Content Management
- Challenges We Faced & Solutions
- 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:
- AI Image Generation: Users need instant, high-quality nail art designs generated from text prompts
- Virtual Try-On: Users want to see how designs look on their actual hands before going to a salon
- Content Discovery: Users need to find relevant designs through intuitive filtering (by color, occasion, technique, etc.)
- SEO & Content: Each design needs rich, SEO-optimized content to rank in search engines
- Performance: Fast loading times, especially on mobile devices
- 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()
);
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
AI: Google Gemini API
Why Gemini?
- Multimodal: Handles both image and text generation
-
Image Generation:
gemini-2.5-flash-image-previewfor creating nail art -
Image-to-Image:
gemini-2.0-flash-expfor virtual try-on -
Content Generation:
gemini-2.5-profor 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 │
└─────────────┘ └─────────────┘ └─────────────┘
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
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;
}
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) });
}
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));
}
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'),
},
};
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.
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.
Query Examples
Get items by category:
const { data } = await supabase
.from('gallery_items')
.select('*')
.eq('category', category)
.order('created_at', { ascending: false })
.limit(12);
Get items by color tag:
const { data } = await supabase
.from('gallery_items')
.select('*')
.contains('colors', ['red']) // PostgreSQL array contains
.limit(12);
Get items with editorial content:
const { data } = await supabase
.from('gallery_items')
.select(`
*,
gallery_editorials (*)
`)
.eq('id', itemId)
.single();
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
}
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 });
}
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'}
/>
);
}
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
});
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;
}
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);
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>
);
}
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 */
`
}} />
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],
},
};
}
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,
},
})),
};
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>`;
}
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'}`;
}
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>`;
}
Sitemap Types:
-
Static Sitemap (
sitemap-static.xml): Core pages (homepage, gallery, categories index) -
Designs Sitemap (
sitemap-designs.xml): All individual design pages (highest priority: 0.8) -
Categories Sitemap (
sitemap-categories.xml): Category filtering pages, color pages, technique pages -
Images Sitemap (
sitemap-images.xml): Image metadata for Google Images -
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
}
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 });
}
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;
}
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}`
]);
}
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)],
},
};
}
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),
}
}))
}
};
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
}
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 || [])
];
}
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(',')
};
}
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' }
});
}
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"
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:
-
Indexing Status:
- Number of pages indexed
- Indexing rate (new pages)
- Coverage issues
-
Rankings:
- Keyword rankings for primary terms
- Category page rankings
- Design page rankings
-
Traffic Sources:
- Organic search traffic
- Pinterest referrals
- Direct traffic
- Social media
-
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:
- Generate SEO metadata
- Create structured data
- Update sitemaps
- Submit to IndexNow
- Generate internal links
- 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 });
}
Benefits:
- Zero manual SEO work
- Consistent SEO quality
- Instant indexing
- Comprehensive optimization
Virtual Try-On Feature
User Flow
- User selects a design from gallery
- User uploads hand photo
- AI applies design to nails
- 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>
);
}
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);
});
}
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);
}
}
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
});
}
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,proonly 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
- User Accounts: Save favorite designs, personal galleries
- Social Sharing: Share designs on Pinterest, Instagram
- Advanced Filters: Filter by difficulty, time, cost
- AR Try-On: Real-time AR using camera
- Design Customization: Edit colors, add patterns
- Community: User-submitted designs, ratings
- Salon Integration: Book appointments directly
Technical Improvements
- GraphQL API: More flexible queries
- Real-time Updates: WebSocket for live gallery updates
- Advanced Caching: Redis for query caching
- Image Processing: On-the-fly image transformations
- Analytics: User behavior tracking
- A/B Testing: Test different UI variations
Key Takeaways
- Choose the right tools: Next.js + Supabase + Cloudflare R2 + Gemini = perfect stack for this use case
- Optimize early: Performance optimizations should be considered from day one
- SEO matters: With 1,600+ pages, proper SEO implementation is crucial
- AI is expensive: Rate limiting and caching are essential
- Mobile first: Most users will be on mobile, optimize accordingly
- 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)