Generate 200+ SEO-optimized salon profiles in minutes — not weeks — by wiring EmDash's Auto Blog/SEO plugin to your D1 database and an LLM provider like OpenRouter.
The Problem — Manual salon content creation doesn't scale
AiSalonHub lets users try on nail designs virtually using AR technology, then book at a real salon. Each salon listed on the platform needs a unique, keyword-rich profile: location details, service menus, pricing tiers, stylist bios, and a description that ranks in local search.
Manually writing 200+ salon profiles is a non-starter. A single profile takes 20–30 minutes of research and copywriting — call that 6–10 hours for 20 profiles. For 200+ salons across multiple cities, you're looking at 100+ hours of purely mechanical work. Updates when salons change their menus or pricing add even more drag.
The core tension: content marketing drives salon discovery, but content production bottlenecks destroy velocity. When every new listing requires a copywriter, you can't scale beyond your first city.
The Solution — EmDash Auto Blog/SEO plugin
EmDash CMS ships with an Auto Blog/SEO plugin that turns this problem into a configuration exercise. The plugin hooks into your EmDash instance and generates content from a configurable LLM provider. Instead of writing profiles by hand, you define:
- **A content template** — the structure each profile should follow
- **An LLM provider** — OpenRouter gives access to GPT-4o, Claude 3.5 Sonnet, and cheaper models for batch work
- **A data source** — the D1 database table holding your salon records
- **A generation trigger** — manual batch, scheduled cron, or webhook on new record
The plugin writes directly to your EmDash content collection, making profiles immediately live on your Astro front-end. No copy-paste, no export-import, no manual HTML wrangling.
Architecture — Plugin hooks into D1, generates SEO-optimized salon descriptions
Here is the data flow:
```
Salon Records (D1)
|
v
EmDash Auto Blog/SEO Plugin
|
├─ Reads: salon name, city, services, pricing, lat/lng
├─ Fetches: LLM completion from OpenRouter
└─ Writes: SEO-optimized profile → EmDash content collection
|
v
Astro front-end renders profile at /salons/{slug}
```
The plugin architecture is clean: a Hook fires after a D1 query returns salon records, passes structured data to a configurable LLM prompt, and the completion is parsed into EmDash content fields (title, body, excerpt, meta description, featured image alt text).
Table: Plugin configuration parameters
| Parameter | Description | Example value |
|---|---|---|
| `collection` | Target EmDash content collection | `"salon_profiles"` |
| `provider` | LLM provider | `"openrouter"` |
| `model` | Model identifier | `"openai/gpt-4o-2024-11-20"` |
| `temperature` | Output creativity (0.0–1.0) | `0.3` |
| `max_tokens` | Max response length | `800` |
| `batch_size` | Records per generation run | `10` |
| `prompt_template` | Jinja2-style template | `"Write a salon profile for {name} in {city}..."` |
| `d1_query` | SQL to fetch salon records | `"SELECT * FROM salons WHERE has_profile = 0"` |
Step-by-Step Implementation
1. Registering the plugin
In your EmDash configuration file (`emdas.config.ts`), import and register the Auto Blog/SEO plugin:
```ts
// emdas.config.ts
import { defineConfig } from 'emdas';
import { autoBlogPlugin } from '@emdas/plugin-auto-blog';
export default defineConfig({
plugins: [
autoBlogPlugin({
collections: ['salon_profiles'],
}),
],
});
```
2. Configuring the LLM provider (OpenRouter)
OpenRouter gives you a single API for 200+ models with built-in fallback routing. Set your API key in environment variables and configure the provider:
```ts
// providers/openrouter.ts
import { defineProvider } from '@emdas/plugin-auto-blog';
export const openRouterProvider = defineProvider({
name: 'openrouter',
apiKey: process.env.OPENROUTER_API_KEY,
baseUrl: 'https://openrouter.ai/api/v1',
defaultModel: 'openai/gpt-4o-2024-11-20',
defaultParams: {
temperature: 0.3,
max_tokens: 800,
},
headers: {
'HTTP-Referer': 'https://aisalonhub.com',
'X-Title': 'AiSalonHub',
},
});
```
The custom headers help OpenRouter attribute usage and maintain rate-limit priority for your domain.
3. Setting content templates per salon category
Salons fall into categories: budget-friendly, luxury, specialty (nail art), and mobile services. Each category needs a different angle. Define templates as an array:
```ts
const salonTemplates = [
{
category: 'luxury',
prompt: `Write a 150-word salon profile for {name} in {city}, {state}.
Focus on: premium nail services, high-end product lines (OPI, Dior, Chanel),
ambiance, and VIP booking experience. Include a strong local SEO keyword
like "{city} luxury nail salon" in the first paragraph.
Services offered: {services}
Price range: {price_range}`,
tags: ['luxury', 'premium-nails', '{city}'],
meta_template: '{name} — Luxury Nail Salon in {city} | AiSalonHub',
},
{
category: 'budget',
prompt: `Write a 120-word salon profile for {name} in {city}, {state}.
Focus on: affordable pricing, walk-in availability, quick service,
and student discounts. Local SEO keyword: "{city} cheap nails".
Services offered: {services}
Price range: {price_range}`,
tags: ['budget-friendly', 'affordable-nails', '{city}'],
meta_template: '{name} — Affordable Nail Salon in {city} | AiSalonHub',
},
{
category: 'specialty',
prompt: `Write a 180-word salon profile for {name} in {city}, {state}.
Focus on: custom nail art, 3D designs, hand-painted techniques,
and trend-forward styles. SEO keyword: "{city} nail art studio".
Services offered: {services}
Price range: {price_range}`,
tags: ['nail-art', 'specialty', '{city}'],
meta_template: '{name} — Custom Nail Art in {city} | AiSalonHub',
},
];
```
Each template uses `{placeholders}` that get replaced with real data from your D1 salon records at generation time.
4. Batch generation from D1 salon records
Now wire it together. Run a batch generation that queries D1 for salons missing a profile and generates one per record:
```ts
// scripts/generate-salon-profiles.ts
import { runBatchGeneration } from '@emdas/plugin-auto-blog';
import { openRouterProvider } from '../providers/openrouter';
import { salonTemplates } from '../templates/salon-templates';
export async function generateAllSalonProfiles() {
const result = await runBatchGeneration({
provider: openRouterProvider,
d1Query: `
SELECT s.*, sc.category
FROM salons s
JOIN salon_categories sc ON s.category_id = sc.id
WHERE s.has_profile = 0 OR s.profile_stale = 1
LIMIT 50
`,
templates: salonTemplates,
onProgress: (done, total, current) => {
console.log(`[${done}/${total}] Generating profile for ${current.name}`);
},
onComplete: async (results) => {
await markProfilesComplete(results.map(r => r.salon_id));
},
});
console.log(`Generated ${result.generated} profiles in ${result.elapsedMs}ms`);
return result;
}
```
Run it on deploy or as a cron worker:
```bash
Manual trigger
npx emdas run generate-salon-profiles
Or as a Cloudflare Workers cron (wrangler.toml)
[triggers]
crons = ["0 6 * * 1"] # Every Monday at 6 AM
```
Results — 200+ salon profiles generated, indexed in Google
After deploying this pipeline on AiSalonHub, here is what we measured over 4 weeks:
| Metric | Before (manual) | After (automated) | Improvement |
|---|---|---|---|
| Profiles created | 18 in 3 weeks | 214 in 1 week | +1,088% |
| Time per profile | ~25 min | ~12 seconds | 125x faster |
| Google indexed profiles | 12 | 198 | +1,550% |
| Organic impressions (Search Console) | 340 / week | 4,200 / week | +1,135% |
| Cost per profile | $8.33 (writer) | $0.04 (LLM tokens) | 99.5% reduction |
The pipeline cost roughly $8.56 in OpenRouter credits to generate all 214 profiles at GPT-4o pricing. The same volume at freelance rates would have been north of $1,700.
More importantly, the generated profiles rank. Google indexed 198 of 214 within 10 days. Organic impressions went from 340 to 4,200 per week — before any link-building or promotion. Structured local SEO content drove discovery naturally.
Key Takeaways
1. **Automate before you scale.** Manual content creation is a trap at 10+ profiles per batch. The EmDash Auto Blog/SEO plugin removes the bottleneck without removing quality control.
2. **Template diversity matters.** Running all salons through the same prompt produces generic copy. Separate templates for luxury, budget, and specialty categories kept profiles differentiated and relevant to different search intents.
3. **LLM cost is negligible for batch generation.** At $0.04 per profile, you can regenerate an entire city's salon listings for the price of a coffee.
4. **D1 as the source of truth keeps everything in sync.** Salons update their pricing or services in the database; the next cron run picks up changes and regenerates stale profiles automatically.
5. **Content marketing + product synergy.** Generated profiles don't just drive SEO traffic — they feed directly into the AR try-on experience. A user searches "nail art in Austin," lands on a salon profile, and clicks "Try On" to see designs on their own nails. The pipeline closes the loop from discovery to conversion.
6. **Monitoring is essential.** Not every generated profile is perfect. We added a `profile_quality` score checking for hallucinated services, wrong addresses, or broken links with a threshold of 0.7. Profiles below that get flagged for human review. The plugin's `onComplete` hook makes this trivial.
---
*AiSalonHub is built on EmDash CMS + Astro, deployed on Cloudflare Workers. The Auto Blog/SEO plugin is available as @emdas/plugin-auto-blog on npm. Give your salons the content they deserve — without the manual grind.*