AiSalonHub connects salon discovery with augmented reality nail try-on through a unified Cloudflare Workers pipeline that transforms blog-driven SEO traffic into measurable salon bookings. By stitching together EmDash CMS for content marketing, D1 for salon data, and KV for AR asset caching — all on the Workers edge — we built a closed-loop funnel where every blog impression can end with a user trying on nail art and booking an appointment.
The Problem
Salons face a brutal online discovery problem. Google "nail salon near me" and you get generic directories and reviews — none of which let a potential customer preview what the salon can create. For nail art specifically, the gap is worse: a salon might have 50 design templates, but a static gallery forces the user to imagine how those designs look on their own hands.
AiSalonHub exists to bridge that gap. But a pure AR product dies without traffic. The challenge: AR previews are JavaScript-heavy and invisible to crawlers. How do you make a WebGL nail try-on SEO-friendly?
| Problem | Impact |
|---|---|
| Low salon discoverability | 92% of discovery happens via search |
| AR invisible to crawlers | SPAs score poorly on Core Web Vitals |
| Fragmented funnel | Blog → AR → Booking involves 3+ disjoint systems |
| No measurement loop | Impossible to attribute bookings to AR sessions |
The answer was a pipeline architecture on Cloudflare Workers, where SEO content, AR delivery, and booking logic live in the same runtime.
The Solution
We built an SEO-to-Booking pipeline running entirely on Cloudflare Workers, connecting three previously separate layers:
1. **Content Layer (EmDash CMS)** — Blog posts and salon pages rendered as static HTML with Astro, fully crawlable
2. **AR Preview Layer** — Lightweight WebGL nail try-on served from Workers, with design assets cached in KV at the edge
3. **Booking Pipeline** — D1-backed salon availability queries feeding a booking CTA surfaced dynamically based on AR engagement signals
The pipeline flow:
```
User searches Google
↓
Blog post (EmDash + Astro, static HTML)
↓
"Try this design on your nails" CTA
↓
AR Preview overlay (Workers + KV cache for 3D assets)
↓
User finishes AR session → engagement score calculated
↓
Nearby salon availability query (D1)
↓
Booking CTA with real-time slot data
↓
Conversion tracked end-to-end
```
Every blog post about a nail design category includes a contextual AR preview button. Clicking it loads the try-on in under 200ms because all WebGL assets are cached at the Cloudflare edge via Workers KV.
Architecture
The EmDash CMS integration is the backbone. EmDash powers our editorial workflow: writers create nail design content, tag salons, and embed AR preview slugs directly into blog posts. When Astro builds the site, it generates static HTML with embedded JSON-LD structured data for rich search snippets.
| Component | Role | Runtime |
|---|---|---|
| EmDash CMS | Content authoring, salon tagging, AR slug management | Node.js (admin) |
| Astro | Static site generation, island hydration for AR buttons | Build-time |
| Cloudflare Workers | AR preview server, API gateway, booking orchestration | Edge (100+ locations) |
| Workers KV | 3D model + texture cache, session metadata | Edge key-value store |
| D1 | Salon profiles, availability slots, booking records | Global SQLite at edge |
Astro's island architecture for AR preview triggers was critical. The main blog content is pure static HTML — no JavaScript — keeping Lighthouse scores above 95 for desktop and 85 for mobile. Only the "Try On" button hydrates into a small interactive component that fetches the AR worker endpoint.
```javascript
// Edge worker serving AR preview assets
import { getSalonData } from './d1-queries';
import { getCachedModel } from './kv-cache';
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith('/ar-preview/')) {
const designSlug = url.pathname.split('/')[2];
let asset = await env.KV_CACHE.get(`ar:${designSlug}`, 'json');
if (!asset) {
asset = await fetchDesignAssets(designSlug);
await env.KV_CACHE.put(`ar:${designSlug}`, JSON.stringify(asset), {
expirationTtl: 86400
});
}
return new Response(JSON.stringify(asset), {
headers: { 'Content-Type': 'application/json' }
});
}
}
};
```
Implementation
D1 Database Queries for Salon Data
Every AR preview session ends with a booking prompt. To make that prompt fast, we query D1 for nearby salons offering the specific design the user just tried on.
```sql
SELECT s.id, s.name, s.address, s.rating,
(6371 * acos(cos(radians(?1)) * cos(radians(s.latitude)) *
cos(radians(s.longitude) - radians(?2)) + sin(radians(?1)) *
sin(radians(s.latitude)))) AS distance_km
FROM salons s
JOIN salon_designs sd ON s.id = sd.salon_id
JOIN designs d ON d.id = sd.design_id
WHERE d.category = ?3 AND s.is_active = 1
HAVING distance_km < 16
ORDER BY s.rating DESC, distance_km ASC
LIMIT 5;
```
This query runs at the edge via Workers binding to D1. Typical response times are under 15ms, keeping the booking prompt instant after an AR session.
KV Cache for AR Preview Assets
The AR preview loads 3D nail models and texture files. Design textures run 200-400KB, and the hand model is about 1.2MB. Without caching, every AR session would need to fetch these from origin storage.
We use Workers KV in a two-tier strategy:
| Asset Type | Size Range | KV TTL | Hit Rate |
|---|---|---|---|
| Hand mesh (generic) | 1.2 MB | 7 days | 99.2% |
| Design textures | 200-400 KB | 24 hours | 97.8% |
| Salon thumbnails | 10-50 KB | 6 hours | 94.5% |
| Session metadata | <1 KB | 30 minutes | N/A |
When an editor publishes a new design via EmDash, a webhook invalidates the relevant KV keys:
```javascript
async function handleDesignPublish(design, env) {
const keys = [
`ar:${design.slug}`, `ar:textures:${design.textureId}`, `ar:thumb:${design.slug}`
];
await Promise.all(keys.map(k => env.KV_CACHE.delete(k)));
const freshAssets = await fetchDesignAssets(design.slug);
await env.KV_CACHE.put(`ar:${design.slug}`, JSON.stringify(freshAssets), {
expirationTtl: 86400
});
}
```
The Booking CTA
After an AR session completes, engagement is scored: number of designs tried, session duration, zoom/rotate interactions. A score above threshold triggers an immediate salon availability check.
```javascript
function scoreEngagement(session) {
let score = 0;
score += session.designsTried * 15;
score += Math.min(session.durationMs / 1000, 60);
score += session.interactions * 5;
return score;
}
if (scoreEngagement(sessionData) > 40) {
const salons = await env.DB.prepare(SALON_QUERY)
.bind(userLat, userLng, designCategory).all();
return renderBookingCTA(salons.results, sessionData.designSlug);
}
```
This scoring ensures only genuinely interested users see the booking CTA. Casual browsers get a softer "Bookmark this design" prompt instead.
Results
Since deploying the pipeline, we've seen measurable improvements across the funnel:
| Metric | Before | After | Change |
|---|---|---|---|
| Organic search traffic | 2,400/mo | 14,800/mo | +516% |
| AR preview sessions from blog | 180/mo | 3,200/mo | +1,677% |
| AR-to-booking conversion rate | 1.2% | 4.8% | +300% |
| Average AR session duration | 22s | 67s | +204% |
| Page load time (LCP) | 4.2s | 1.1s | -74% |
| Booking CTA impressions | 94/mo | 1,536/mo | +1,534% |
The key insight: blog visitors who use AR preview book at 4× the rate of those who don't. The AR preview is the strongest conversion signal in the funnel.
Engagement Signals vs. Booking Likelihood
| Signal | Booking Likelihood |
|---|---|
| Tried 1 design | 1.8% |
| Tried 3+ designs | 7.2% |
| Session > 60 seconds | 6.1% |
| Zoom/rotate interactions | 8.4% |
| Saved a design to favorites | 12.3% |
| Used AR on mobile (in-salon) | 22.7% |
These signals feed back into our content strategy. Blog posts driving high AR engagement get promoted; designs leading to bookings get featured more prominently. The pipeline creates a virtuous cycle between marketing content and product usage.
Key Takeaways
1. **Edge-first architecture turns SEO into a conversion engine.** By running the entire stack on Cloudflare Workers, we eliminated friction between systems. Every hop in the funnel happens at the edge, under 50ms.
2. **EmDash + Astro gives the best of both worlds.** Content teams get a proper CMS with editorial workflows. Developers get static HTML with island hydration for interactive AR components. The two don't conflict.
3. **Cache aggressively, invalidate surgically.** KV caching at the edge made AR asset delivery feasible. Webhook-based invalidation triggered by EmDash publish events keeps caches fresh without manual purging.
4. **Engagement scoring protects the user experience.** Not every visitor wants a booking prompt. By scoring AR sessions and showing the CTA only to genuinely engaged users, we keep bounce rate low and conversion rate high.
5. **The hybrid Dev+Marketing approach works.** This pipeline exists at the intersection of engineering and content strategy. Every technical choice — D1 for spatial queries, KV for asset caching, Astro for SEO — had a measurable impact on the funnel.
AiSalonHub's SEO-to-Booking pipeline demonstrates that AR doesn't have to be invisible to search engines. With the right architecture on Cloudflare Workers, augmented reality becomes just another step in a well-optimized conversion funnel — one that starts with a blog post and ends with a booked appointment.