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.*