Why Structured Content Matters for Local Service Directories
When you're building a directory that helps people find nail salons, the difference between a useful site and a frustrating one comes down to how well you structure your content. AiSalonHub is designed to connect salon customers with the perfect salon for their needs, and that means every service, every price point, and every location detail needs to be surfaced in a way that both humans and search engines can understand.
Structured content isn't just about clean database schemas. It's about building a discovery engine that can answer questions like "Which salon near me does gel manicures under $40?" or "Who offers SNS dipping powder in the Lincoln Park neighborhood?" without requiring the user to browse through dozens of individual salon pages.
The Catalog Schema
AiSalonHub's service catalog is built around a `services` collection in EmDash CMS, with each entry containing:
- **Service name** — Standardized naming to enable comparison ("Gel Manicure", not "Gel Nails Special")
- **Category** — Taxonomized into parent groups (Manicure, Pedicure, Nail Art, Waxing, etc.)
- **Price range** — Stored as min/max integers for filtering and sorting
- **Duration** — Minutes, enabling appointment time estimation
- **Salon reference** — Foreign key to the salon's entry in the `comparisons` collection
- **Description** — Portable Text for flexible formatting
The key insight is that services are first-class entities, not nested JSON blobs inside salon entries. This lets us run queries like "find all salons offering gel manicures under $50 within 5 miles" using D1's SQL engine, which is far more efficient than filtering at the application layer. Separating services into their own collection also makes the admin UI more manageable — salon owners can bulk-edit their service catalog independently of their profile details.
Cloudflare D1 as the Query Engine
D1's SQLite-based architecture is a natural fit for structured catalog queries. Here's how AiSalonHub handles a typical discovery query:
```sql
-- Find salons offering gel manicures near a ZIP code
SELECT s.name, s.price_min, s.price_max,
sal.name AS salon_name,
sal.address, sal.rating
FROM services s
JOIN comparisons sal ON s.salon_id = sal.id
WHERE s.category = 'gel-manicure'
AND s.price_max <= 50
AND sal.zip_code LIKE '606%'
AND sal.status = 'published'
ORDER BY sal.rating DESC
LIMIT 20
```
This query runs in under 2ms even with thousands of services indexed, thanks to D1's B-tree indexes on `category`, `price_max`, and `salon_id`. We also added a composite index on `(salon_id, category)` for the salon detail pages that need to show all services for a single salon at once.
Faceted Search Implementation
The search experience on AiSalonHub uses faceted filtering — visitors can narrow results by category, price range, neighborhood, and rating simultaneously. Rather than using a dedicated search engine like Algolia or MeiliSearch, we built the faceted search layer entirely in SQL on D1:
```typescript
// Build dynamic WHERE clause from active filters
const conditions: string[] = ['sal.status = ?'];
const params: any[] = ['published'];
if (filters.category) {
conditions.push('s.category = ?');
params.push(filters.category);
}
if (filters.maxPrice) {
conditions.push('s.price_max <= ?');
params.push(filters.maxPrice);
}
if (filters.zipPrefix) {
conditions.push('sal.zip_code LIKE ?');
params.push(filters.zipPrefix + '%');
}
const sql = `
SELECT s.*, sal.name, sal.rating, sal.address
FROM services s
JOIN comparisons sal ON s.salon_id = sal.id
WHERE ${conditions.join(' AND ')}
ORDER BY sal.rating DESC
LIMIT 30
`;
```
The results are cached at the Cloudflare edge with a 5-minute TTL, which keeps D1 reads under 10 per minute even during peak traffic. For a niche directory, this completely eliminates the need for a dedicated search service.
The Comparison Engine
AiSalonHub's comparison engine is the main differentiator. Rather than showing a flat list of salons, visitors can select 2-3 salons and view a side-by-side comparison of:
- Services offered with prices
- Available appointment times
- Customer ratings and review counts
- Specializations (nail art, dipping powder, acrylics)
- Location and parking info
The comparison view is rendered server-side using EmDash's Portable Text components, with structured data injected as JSON-LD for Google's rich snippet system. This dual approach means the comparison is both human-readable and machine-parsable.
JSON-LD for Rich Search Results
Every service page automatically generates JSON-LD structured data:
```json
{
"@context": "https://schema.org",
"@type": "Service",
"name": "Gel Manicure",
"provider": {
"@type": "LocalBusiness",
"name": "Luxe Nail Bar",
"address": {
"@type": "PostalAddress",
"addressLocality": "Chicago",
"addressRegion": "IL"
}
},
"offers": {
"@type": "Offer",
"price": "35.00",
"priceCurrency": "USD"
}
}
```
This structured data helped early test pages appear in Google's "Services near me" carousel within two weeks of publishing, driving a measurable increase in click-through rates for listed salons. Each page also includes breadcrumb markup and aggregate rating data, making the entire site eligible for Google's enhanced listing features.
Caching Strategy
Service catalog pages are cached at the Cloudflare edge with a 15-minute TTL using Astro's `cacheHint()` API. Catalog-specific queries bypass the cache when a salon updates its pricing (triggered via a webhook that purges the relevant URL from Cloudflare's cache API). This strikes the right balance between freshness and performance — most salon pricing doesn't change daily, so the cache hit rate stays above 85%.
What We Learned
Building a structured service catalog taught us that schema design is a marketing decision, not just a technical one. Every field you add to a service entry is a potential search filter, a comparison dimension, or an SEO signal. The most successful directories treat their database schema as a product feature, not backend plumbing.
For AiSalonHub, the structured content approach turned a simple salon listing into a discovery engine that serves both customers and salon owners. Customers find exactly what they need faster, and salons get discovered by people who are actively looking for their specific services. When you're bootstrapping a marketplace, that kind of built-in discovery is worth more than any ad campaign.