GBP Post Generator from Completed Jobs
The Problem
Your Google Business Profile has not been updated in months. Google demotes inactive profiles. Your competitor ranks above you because they post regularly. This automation picks the best completed jobs each week and generates neighborhood-specific posts with photos and a call-to-action.
How It Works
Completed jobs from past week (type, neighborhood, photos)
AI picks 3 best jobs, writes neighborhood-specific posts with CTA, formats photos, queues for approval.
3 fresh GBP posts per week on autopilot. Local SEO boost.
Importable Templates
PRD
# Product Requirements Document
## Recipe 006 — GBP Post Creator
### AI Trades Platform
---
Recipe Slug: `gbp-post-creator`
Recipe Number: 006
Difficulty: Replit Build
Time Estimate: 6–8 hours
Category: SEO & Marketing
Revenue Impact: $800–3,200/mo (increased GBP visibility → more calls)
Hours Saved: 4 hrs/wk
Replaces: Marketing agency GBP posting ($300–1,000/mo) or manual posting (never happens)
Reusable Modules Referenced:
- Module 0: UX Philosophy (`modules/ux-philosophy-module.md`) — foundational design principles for all screens
- Module: Brand Voice Engine (`modules/brand-voice-engine-module.md`) — voice profiling, tone matching, anti-AI writing
- Module: Headless Browser Automation (`modules/headless-browser-automation-module.md`) — website scraping patterns
- Module 9: Onboarding Wizard (`modules/onboarding-wizard-module.md`) — multi-step first-run setup
- Module 10: Settings Panel (`modules/settings-panel-module.md`) — centralized settings management
- Module 11: Notification / Toast (`modules/notification-toast-module.md`) — toasts, badges, notification center
- Module 12: Data Table / List View (`modules/data-table-list-view-module.md`) — post history list
Integration Docs (include with build):
- Google Business Profile (`integrations/google-business-profile.md`) — OAuth, post creation, old post fetching
- Google OAuth (`integrations/google-oauth.md`) — shared OAuth patterns for GBP connection
- OpenAI / Anthropic (`integrations/openai-anthropic.md`) — LLM for post generation, tone analysis, content creation
---
## Table of Contents
4. Tone & Service Area Extraction
5. Anti-AI Writing System for Posts
6. Multi-Shot Learning from User Edits
11. Data Model
13. Technical Stack
---
1. Recipe Overview
The GBP Post Creator helps home service contractors consistently post updates to their Google Business Profile to boost local SEO. It scrapes the contractor's website and old GBP posts to learn their tone, services, and service area — then generates new posts that sound human and are optimized for local search.
The key differentiator: The system doesn't generate generic "We offer plumbing services!" posts. It scrapes the contractor's actual website copy and previous GBP posts, learns their specific services, service area, tone, and seasonal patterns — then generates posts that read like the contractor sat down and wrote them. Every edit the contractor makes teaches the AI to write better posts.
Key Capabilities:
- Scrape contractor's business website for services, service area, tone, and copy patterns
- Scrape old GBP posts for historical tone and content patterns
- Generate SEO-optimized GBP posts using anti-AI writing guidelines + contractor's brand voice
- Support all GBP post types: What's New, Offer, Event
- Contractor edits posts → those edits train the AI (multi-shot learning)
- Post directly to GBP via API (or copy-to-clipboard for manual posting)
- Suggest posting schedule based on SEO best practices
- Multi-language support
- Full backend: auth, PostgreSQL database, storage
---
2. Strategy Brief
The Core Problem & Solution Logic
The Pain: "I know posting on Google helps my SEO but I never have time to write posts and I don't know what to say."
The Diagnosis: Content creation paralysis + time friction. Contractors understand that regular GBP posts improve local search rankings, but they face two blockers: (1) they don't know what to write, and (2) even when they do, writing a post takes 15-20 minutes they don't have.
The Product Fix: A post generator that already knows the contractor's services, service area, and voice — so it can suggest and generate posts automatically. The contractor's job is reduced to: review, tweak if needed, post. 60 seconds per post. The system suggests 2-3 posts per week based on seasonal relevance, service mix, and posting history.
Feature Expansion
- Standard Approach: "AI writes a GBP post about plumbing."
- Strategic Approach: AI analyzes the contractor's actual website ("We serve Mesa, Gilbert, Chandler, and Tempe" + "24/7 emergency drain cleaning, water heater replacement, repiping"), scrapes their old GBP posts for tone and topics they've already covered, then generates a new post that's locally relevant ("Hey Mesa — if your water heater is making that rumbling sound, that's sediment buildup and it's about to cost you. We can flush it same-day."), avoids repeating recent topics, and includes local service area mentions for SEO.
- Reasoning: Generic posts don't rank. Google rewards GBP posts that mention specific services and locations. The scraping step ensures every post is locally relevant without the contractor having to think about SEO.
Screen/UI Implications
- Onboarding: connect GBP → paste website URL → system scrapes and learns → confirm services + service area
- Post dashboard: upcoming/suggested posts, post history, posting streak
- Post editor: generated post in editable textarea, post type selector, image upload, schedule/publish
- Settings: language, posting frequency, service area, GBP connection
Automation Strategy
- Suggest 2-3 posts per week based on:
- Services not posted about recently
- Seasonal relevance (AC in summer, heating in winter, etc.)
- Local events/weather (if data available)
- Weekly notification: "You have 2 posts ready for review"
- Track posting consistency: "You've posted 3 weeks in a row — keep it up"
---
3. Website & GBP Scraping
Website Scraping
Purpose: Extract the contractor's services, service area, tone, and content patterns from their business website.
Pages Targeted:
1. Homepage — business name, tagline, hero copy
2. Services / What We Do — full service list with descriptions
3. About / Our Story — company history, values, personality
4. Service Area / Locations — cities, neighborhoods, zip codes served
5. Testimonials / Reviews (if page exists) — social proof language
Scraping Implementation:
```typescript
interface WebsiteScrapingResult {
business_name: string;
tagline: string;
services: Array<{
name: string;
description: string;
keywords: string[];
}>;
service_area: {
cities: string[];
neighborhoods: string[];
counties: string[];
state: string;
zip_codes: string[];
};
tone_signals: {
formality_level: number;
humor_detected: boolean;
key_phrases: string[];
regional_language: string[];
};
raw_copy: {
homepage: string;
services_page: string;
about_page: string;
service_area_page: string;
};
scrape_metadata: {
pages_scraped: number;
total_chars: number;
scraped_at: string;
};
}
```
Scraping Flow:
```
1. Validate URL (auto-prepend https://)
2. Fetch homepage HTML via Cheerio/fetch
3. Parse navigation links to find Services, About, Service Area pages
- Match patterns: /services/, /what-we-do/, /about/, /our-story/,
/service-area/, /locations/, /areas-we-serve/
4. Fetch each discovered page (max 5 pages)
5. Strip HTML: remove nav, header, footer, scripts, styles
6. Extract structured data:
- Services: look for <h2>/<h3> headings under services page
- Service Area: look for city/state mentions, ZIP codes, "serving" patterns
- Tone: analyze word choice, formality, humor, regional markers
7. Store raw text + structured extraction in database
```
UX During Scraping:
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🔍 Reading your website... │
│ │
│ ✅ Found your homepage │
│ ✅ Found your services page (8 services listed) │
│ 🔄 Reading your service area page... │
│ ⬜ Analyzing your tone and copy... │
│ │
│ This usually takes about 15 seconds. │
│ │
└─────────────────────────────────────────────────────────────┘
```
GBP Post Scraping
Purpose: Analyze the contractor's previous GBP posts to understand their historical tone, topics, and posting patterns.
Data Extracted from Old Posts:
```typescript
interface GBPPostHistory {
posts: Array<{
post_type: 'STANDARD' | 'OFFER' | 'EVENT';
text: string;
publish_date: string;
media_urls: string[];
call_to_action?: {
type: string;
url: string;
};
}>;
analysis: {
total_posts: number;
posting_frequency: string;
avg_post_length: number;
common_topics: string[];
topics_never_covered: string[];
tone_consistency: string;
seasonal_patterns: Array<{
season: string;
topics: string[];
}>;
last_post_date: string;
posting_gap_days: number;
};
}
```
GBP Post Fetching (API Mode):
```typescript
const GBP_V4_BASE = 'https://mybusiness.googleapis.com/v4';
export async function listPosts(
accessToken: string,
accountId: string,
locationId: string
): Promise<GBPPost[]> {
const url = `${GBP_V4_BASE}/accounts/${accountId}/locations/${locationId}/localPosts`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
if (response.status === 403) {
throw new Error('GBP API access denied. Use manual post history.');
}
throw new Error(`Failed to list posts: ${response.status}`);
}
const data = await response.json();
return data.localPosts || [];
}
```
GBP Post Fetching (Manual Fallback):
If API access is unavailable, the contractor can:
1. View their posts at `https://business.google.com/posts/l/{locationId}`
2. Copy-paste 3-5 recent posts into a text area
3. System analyzes the pasted posts for tone and topic patterns
---
4. Tone & Service Area Extraction
Service Area Extraction
The system builds a structured service area profile from website scraping:
```
LLM Analysis Prompt:
Analyze the following website text from a {{TRADE_TYPE}} contractor's website.
Extract their service area into structured data.
Website text:
{{WEBSITE_RAW_TEXT}}
Extract:
1. Cities explicitly mentioned as served
2. Neighborhoods or areas within cities
3. Counties mentioned
4. State(s)
5. ZIP codes (if listed)
6. Service radius (if mentioned, e.g., "within 30 miles of Phoenix")
7. The PRIMARY city (their home base / main market)
Output as JSON matching this schema:
{
"primary_city": "string",
"cities": ["string"],
"neighborhoods": ["string"],
"counties": ["string"],
"state": "string",
"zip_codes": ["string"],
"service_radius": "string or null"
}
```
Tone Extraction from Old Posts
```
LLM Analysis Prompt:
Analyze these {{POST_COUNT}} Google Business Profile posts from a {{TRADE_TYPE}} contractor.
Posts:
{{OLD_POSTS_TEXT}}
Determine:
1. Tone descriptors (3-5 adjectives): Are they casual, professional, funny, direct, warm?
2. Post structure patterns: Do they start with a question? A statement? A seasonal reference?
3. Call-to-action style: Do they say "Call us" or "Give us a ring" or "Book online"?
4. Length preference: Short and punchy (< 100 words) or detailed (200+ words)?
5. Emoji usage: None, minimal, or heavy?
6. Topics covered vs. topics they could cover but haven't
7. Any brand phrases or signatures they consistently use
Output as a tone profile for post generation.
```
Confirmation UI
After scraping, the contractor confirms what was found:
```
┌─────────────────────────────────────────────────────────────┐
│ Here's what we found from your website: │
│ ────────────────────────────────────────────────────────── │
│ │
│ YOUR SERVICES │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ✅ Drain Cleaning ✅ Water Heater Repair │ │
│ │ ✅ Sewer Line Repair ✅ Garbage Disposal │ │
│ │ ✅ Leak Detection ✅ Repiping │ │
│ │ ✅ Faucet Install ✅ Toilet Repair │ │
│ │ │ │
│ │ + Add a service we missed │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ YOUR SERVICE AREA │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Home base: Mesa, AZ │ │
│ │ Also serving: Gilbert, Chandler, Tempe, Scottsdale, │ │
│ │ Apache Junction, Queen Creek │ │
│ │ │ │
│ │ + Add a city ✏️ Edit │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ YOUR TONE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Based on your website and old posts: │ │
│ │ Casual • Direct • Uses humor occasionally │ │
│ │ Formality: 3/10 (friendly and relaxed) │ │
│ │ │ │
│ │ ✏️ Adjust │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [ Looks good — let's make some posts ] │
└─────────────────────────────────────────────────────────────┘
```
---
5. Anti-AI Writing System for Posts
The Problem with AI-Generated GBP Posts
AI GBP posts fail because they sound like marketing brochures, not a real business owner:
| AI Tell | Example | Why It Fails |
|---|---|---|
| Generic opener | "Are you looking for reliable plumbing services?" | Every AI marketing tool writes this |
| Buzzword stuffing | "Our team of certified professionals delivers top-notch solutions" | No contractor talks like a LinkedIn post |
| Empty superlatives | "Best plumbing service in Phoenix!" | Google ignores self-proclaimed "best" claims |
| List of all services | "We offer drain cleaning, water heater repair, sewer line..." | Reads like a Yellow Pages ad |
| Missing local specificity | "Serving the greater Phoenix area" | Too vague — doesn't mention actual neighborhoods |
| Formal tone | "We are pleased to announce our new seasonal promotion" | Contractors don't write press releases |
Anti-AI Writing Rules for Posts
```
ANTI-AI WRITING RULES FOR GBP POSTS — MANDATORY
You are writing a Google Business Profile post for a real contractor.
It must read like the business owner typed it on their phone between jobs.
BANNED OPENINGS:
- "Are you looking for..." (any variation)
- "Is your [thing] giving you trouble?"
- "As the trusted..." or "As your local..."
- "We are proud to..." / "We are excited to..."
- "Attention homeowners!"
- "Did you know that..."
- Any question that sounds like a radio ad
BANNED PHRASES:
- "top-notch" / "best-in-class" / "second to none"
- "certified professionals" / "expert team"
- "state-of-the-art"
- "customer satisfaction is our priority"
- "look no further"
- "don't hesitate to contact"
- "we've got you covered"
- "your comfort is our concern"
- "peace of mind"
- "comprehensive solutions"
- "hassle-free"
BANNED PATTERNS:
- Listing more than 2 services in one post (focus is better than breadth)
- Using hashtags (GBP posts aren't social media)
- More than 1 exclamation mark per post
- Ending with a generic CTA like "Contact us today!"
- Starting with "Hey [City Name]!" (this has become an AI cliché too)
- Blog-style numbered lists ("5 Reasons Your AC...")
REQUIRED HUMAN SIGNALS:
- Use contractions ("we're", "it's", "don't")
- Reference something specific: a season, weather, local landmark, neighborhood
- Keep it conversational — write like you'd text a neighbor
- One topic per post, not a service catalog
- CTA should sound natural: "Give us a call" or "DM us" or just include the phone number
- Mention a specific city/neighborhood from the service area (rotates each post)
- OK to reference a recent job (anonymized) for authenticity
POST LENGTH GUIDELINES:
- "What's New" posts: 50-150 words (short, punchy, one idea)
- Offer posts: 80-200 words (clear offer, expiry, how to claim)
- Event posts: 80-150 words (what, when, where, why care)
GOOD POST EXAMPLES (for calibration):
What's New (plumber, Mesa AZ):
"Quick tip for anyone in Gilbert — if your water pressure dropped in the last week, you're not imagining it. City did some main line work on Baseline. If it doesn't come back in a day or two, could be your pressure regulator. We replaced three of them this week alone. Easy fix, usually under an hour."
What's New (HVAC, Dallas TX):
"106 today in Plano. If your AC is running nonstop but your house won't get below 78, check your filter first. If it's clean and you're still sweating — that's a refrigerant issue, not a thermostat issue. Don't let someone sell you a new unit before checking the charge. We do free diagnostics."
Offer (electrician, Denver CO):
"Running a panel upgrade special through the end of August — $200 off for anyone in Lakewood or Littleton. If you're still on a Federal Pacific or Zinsco panel, those haven't been made in 30 years and your insurance company is going to start asking questions. We'll swap it out in a day."
```
---
6. Multi-Shot Learning from User Edits
Same Pattern as Review Responder
The multi-shot learning system works identically to Recipe 005 (Review Responder), but applied to posts:
| Interaction | What We Store | How It Trains |
|---|---|---|
| Approve without editing | Generated post + "approved_as_is: true" | Good example — add to few-shot pool |
| Edit then approve | AI draft + contractor's edited version | The EDITED version becomes the few-shot example |
| Regenerate | Draft + regeneration count | Signals the first draft was bad |
Few-Shot Example Selection for Posts
```
Selection Criteria:
1. Same post type (What's New, Offer, Event)
2. Same or similar service topic
3. Same season/time of year (if seasonal)
4. Most recent examples first
5. Maximum 3 examples per prompt (posts are longer than review responses)
6. Prefer edited examples over approved-as-is
```
Post Example Schema
```sql
CREATE TABLE post_examples (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES gbp_posts(id) ON DELETE CASCADE,
post_type TEXT NOT NULL,
service_topic TEXT,
season TEXT,
ai_draft_text TEXT NOT NULL,
final_text TEXT NOT NULL,
was_edited BOOLEAN NOT NULL DEFAULT FALSE,
approved_as_is BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(post_id)
);
```
---
7. Post Creation Flow
Core Flow
```
Contractor opens Post Creator
→ Dashboard shows: suggested posts, recent posts, posting streak
→ Taps "Create New Post" or taps a suggested post
→ Post editor: editable textarea with AI-generated content
→ Selects post type (What's New, Offer, Event)
→ Optionally uploads a photo
→ Edits post if needed
→ Taps "Post to Google" (API) or "Copy Post" (manual)
→ Post saved and tracked
```
Post Suggestion Engine
The system suggests posts based on:
```
1. SERVICE ROTATION
- Track which services have been posted about recently
- Suggest services that haven't been mentioned in 30+ days
- Weight by service revenue importance (if known)
2. SEASONAL RELEVANCE
- Summer: AC maintenance, pool equipment, weatherproofing
- Fall: Heating tune-ups, gutter cleaning, winterization
- Winter: Furnace repair, frozen pipe prevention, holiday specials
- Spring: AC pre-season, water heater flush, spring cleaning specials
3. SERVICE AREA ROTATION
- Rotate mentions of different cities/neighborhoods
- Ensures SEO coverage across the full service area
4. POSTING FREQUENCY
- Target: 2-3 posts per week
- If contractor hasn't posted in 7+ days: push notification
- Suggest posting days based on engagement patterns (if data available)
```
Suggested Posts UI
```
┌─────────────────────────────────────────────────────────────┐
│ 📝 Posts │
│ ────────────────────────────────────────────────────────── │
│ │
│ SUGGESTED FOR THIS WEEK │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 💡 Water Heater Maintenance │ │
│ │ Last posted about: 45 days ago │ │
│ │ Suggested for: Gilbert, AZ │ │
│ │ "Winter's coming — here's a quick water heater..." │ │
│ │ │ │
│ │ [ Generate Post ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 🎄 Holiday Hours / Seasonal Offer │ │
│ │ Timely: Holiday season │ │
│ │ Suggested for: Mesa, AZ (primary market) │ │
│ │ "Holiday special — $50 off drain cleaning through..." │ │
│ │ │ │
│ │ [ Generate Post ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ POSTING STREAK: 🔥 3 weeks │
│ [ + Create Custom Post ] │
│ │
│ RECENT POSTS │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Oct 15 — "Quick tip for anyone in Chandler..." │ │
│ │ Oct 12 — "Running a panel upgrade special..." │ │
│ │ Oct 8 — "106 today in Mesa..." │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
Post Editor
Feature: Post Editor
- Visual: Full-screen editor on mobile. Post type selector at top. Large editable textarea with the generated post. Character count. Optional image upload area. Publish/Copy button at bottom.
- Trigger: Contractor taps "Generate Post" on a suggestion or "Create Custom Post".
- Condition: Voice profile must exist (or default used). Service data should be loaded from scraping.
- Result: AI generates a post. Streams into editable textarea. Contractor edits if needed.
```
┌─────────────────────────────────────────────────────────────┐
│ ← Back Preview │
│ │
│ POST TYPE: [ What's New ▾ ] │
│ │
│ SERVICE TOPIC: [ Water Heater ▾ ] │
│ TARGET AREA: [ Gilbert, AZ ▾ ] │
│ │
│ ┌─ YOUR POST ──────────────────────────────────────────┐ │
│ │ │ │
│ │ Quick tip for anyone in Gilbert — if your water │ │
│ │ pressure dropped this week, check your pressure │ │
│ │ regulator before calling anyone. We replaced three │ │
│ │ of them this week. Usually under an hour, and your │ │
│ │ showers will thank you. │ │
│ │ │ │
│ │ (editable — your changes teach the AI your style) │ │
│ └───────────────────────────────────────────────────────┘ │
│ 87 / 300 characters ✓ Good length │
│ │
│ 📷 ADD PHOTO (optional, but recommended for engagement) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [ Upload Photo ] or [ Use Stock Photo ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ CALL TO ACTION: [ Call Now ▾ ] Phone: [(555) 123-4567] │
│ │
│ [ 🔄 Regenerate ] [ 📋 Copy Post ] [ 📤 Post Now ] │
│ │
└─────────────────────────────────────────────────────────────┘
```
Publishing to GBP
GBP Post API (if access granted):
```typescript
const GBP_V4_BASE = 'https://mybusiness.googleapis.com/v4';
interface CreatePostInput {
summary: string;
topicType: 'STANDARD' | 'OFFER' | 'EVENT';
callToAction?: {
actionType: 'CALL' | 'BOOK' | 'LEARN_MORE' | 'ORDER' | 'SHOP';
url?: string;
};
media?: Array<{
mediaFormat: 'PHOTO' | 'VIDEO';
sourceUrl: string;
}>;
offer?: {
couponCode?: string;
redeemOnlineUrl?: string;
termsConditions?: string;
};
event?: {
title: string;
schedule: {
startDate: { year: number; month: number; day: number };
startTime?: { hours: number; minutes: number };
endDate: { year: number; month: number; day: number };
endTime?: { hours: number; minutes: number };
};
};
}
export async function createPost(
accessToken: string,
accountId: string,
locationId: string,
post: CreatePostInput
): Promise<GBPPost> {
const url = `${GBP_V4_BASE}/accounts/${accountId}/locations/${locationId}/localPosts`;
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(post),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to create post: ${JSON.stringify(error)}`);
}
return response.json();
}
```
Manual Mode: Copy post text to clipboard. Provide direct link to GBP posting interface.
---
8. Post Types & Templates
What's New (STANDARD)
Purpose: General updates, tips, seasonal advice, recent job highlights.
AI Generation Prompt Context:
```
Post type: What's New
Service topic: {{SERVICE_TOPIC}}
Target area: {{TARGET_CITY}}
Season: {{CURRENT_SEASON}}
Contractor services: {{SERVICE_LIST}}
Recent post topics (avoid repeats): {{RECENT_TOPICS}}
Write a casual, helpful post about {{SERVICE_TOPIC}} relevant to homeowners in {{TARGET_CITY}}.
Include a practical tip, local reference, or recent observation.
```
Offer
Purpose: Promotions, discounts, seasonal specials.
Additional Fields:
- Offer title
- Discount amount or description
- Start/end date
- Coupon code (optional)
- Terms (optional)
AI Generation Prompt Context:
```
Post type: Offer / Promotion
Service: {{SERVICE_TOPIC}}
Target area: {{TARGET_CITY}}
Discount: {{DISCOUNT_DETAILS}}
Expiry: {{OFFER_END_DATE}}
Write a promotional post that sounds like a real contractor offering a deal,
not a marketing agency running a campaign. Keep it casual and direct.
Include the specific offer details and how to claim it.
```
Event
Purpose: Community events, workshops, open houses.
Additional Fields:
- Event title
- Date/time
- Location
- Description
---
9. Onboarding Flow
Onboarding Wizard Steps
```
Step 1: Connect Google Business Profile
→ Same OAuth flow as Review Responder
→ Pulls old posts for analysis
→ Outcome: GBP connected + post history analyzed
Step 2: Enter Your Website
→ Paste website URL
→ System scrapes and extracts services, service area, tone
→ Contractor confirms/edits the extracted data
→ Outcome: Service profile built
Step 3: Set Language & Posting Preferences
→ Language selector (same as Review Responder)
→ Posting frequency preference: 1/week, 2/week, 3/week
→ Best days to post (or "suggest for me")
→ Outcome: Posting schedule configured
Step 4: Generate First Post
→ System generates a sample "What's New" post
→ Contractor reviews: "Does this sound like you?"
→ Edit and approve to seed the multi-shot examples
→ Outcome: First example created, system calibrated
```
Onboarding UI
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ ●━━━●━━━○━━━○ │
│ Step 2 of 4 │
│ │
│ What's your website? │
│ │
│ We'll read your site to learn your services, service area, │
│ and how you talk to customers. Takes about 15 seconds. │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ https://smithplumbing.com │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 🔍 Grab My Info From My Site │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [ I don't have a website → ] │
│ (We'll ask you a few questions instead) │
│ │
└─────────────────────────────────────────────────────────────┘
```
No Website Fallback:
If the contractor doesn't have a website, a simple form collects:
1. Business name
2. Trade type (dropdown)
3. Services offered (multi-select from common list + custom add)
4. Cities served (text input, comma-separated)
5. How they'd describe their communication style (casual/professional/somewhere in between)
---
10. Settings & Language
Language Settings
Same implementation as Review Responder (Recipe 005). Language selector in Settings. Auto-detection of review language with override option.
Post-Specific Settings
```
┌─────────────────────────────────────────────────────────────┐
│ ⚙️ Settings │
│ ────────────────────────────────────────────────────────── │
│ │
│ GOOGLE CONNECTION │
│ (Same as Review Responder) │
│ │
│ LANGUAGE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Post Language: [ English ▾ ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ POSTING SCHEDULE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Posts per week: [ 2 ▾ ] │ │
│ │ Preferred days: [ Tue, Thu ▾ ] │ │
│ │ Remind me to post: [ ✓ On ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ SERVICES & SERVICE AREA │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Services: 8 services configured │ │
│ │ Service Area: Mesa, Gilbert, Chandler + 4 more │ │
│ │ [ Edit Services ] [ Edit Service Area ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ YOUR VOICE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Voice Profile: Active │ │
│ │ Post Examples: 8 approved posts │ │
│ │ [ Edit Voice Profile ] [ View Post Examples ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WEBSITE │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Website: smithplumbing.com │ │
│ │ Last scraped: 3 days ago │ │
│ │ [ Re-scrape Website ] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
11. Data Model
Migration Script
```sql
-- migrations/002_gbp_post_creator.sql
-- Website scraping results
CREATE TABLE website_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
website_url TEXT NOT NULL,
business_name_extracted TEXT,
tagline_extracted TEXT,
services JSONB DEFAULT '[]'::jsonb,
service_area JSONB DEFAULT '{}'::jsonb,
tone_signals JSONB DEFAULT '{}'::jsonb,
raw_copy JSONB DEFAULT '{}'::jsonb,
pages_scraped INTEGER DEFAULT 0,
last_scraped_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- GBP post history (scraped or API-fetched)
CREATE TABLE gbp_post_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
google_post_id TEXT UNIQUE,
post_type TEXT NOT NULL DEFAULT 'STANDARD' CHECK (
post_type IN ('STANDARD', 'OFFER', 'EVENT')
),
post_text TEXT NOT NULL,
publish_date TIMESTAMPTZ,
media_urls JSONB DEFAULT '[]'::jsonb,
call_to_action JSONB,
source TEXT DEFAULT 'api' CHECK (source IN ('api', 'manual_paste', 'app_created')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_post_history_date ON gbp_post_history(publish_date DESC);
-- Generated posts (current/draft/published)
CREATE TABLE gbp_posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_type TEXT NOT NULL DEFAULT 'STANDARD' CHECK (
post_type IN ('STANDARD', 'OFFER', 'EVENT')
),
status TEXT NOT NULL DEFAULT 'draft' CHECK (
status IN ('suggested', 'draft', 'approved', 'published', 'failed')
),
service_topic TEXT,
target_city TEXT,
season TEXT,
ai_draft_text TEXT,
final_text TEXT,
was_edited BOOLEAN DEFAULT FALSE,
regeneration_count INTEGER DEFAULT 0,
call_to_action_type TEXT,
call_to_action_url TEXT,
image_url TEXT,
offer_details JSONB,
event_details JSONB,
google_post_id TEXT,
published_at TIMESTAMPTZ,
published_via TEXT CHECK (published_via IN ('api', 'manual_copy')),
model_used TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_posts_status ON gbp_posts(status);
CREATE INDEX idx_posts_published ON gbp_posts(published_at DESC);
-- Post examples (multi-shot learning)
CREATE TABLE post_examples (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES gbp_posts(id) ON DELETE CASCADE,
post_type TEXT NOT NULL,
service_topic TEXT,
season TEXT,
ai_draft_text TEXT NOT NULL,
final_text TEXT NOT NULL,
was_edited BOOLEAN NOT NULL DEFAULT FALSE,
approved_as_is BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(post_id)
);
CREATE INDEX idx_post_examples_type ON post_examples(post_type);
-- Posting schedule / streak tracking
CREATE TABLE posting_schedule (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
posts_per_week INTEGER DEFAULT 2,
preferred_days TEXT[] DEFAULT '{"Tuesday","Thursday"}',
remind_to_post BOOLEAN DEFAULT TRUE,
current_streak_weeks INTEGER DEFAULT 0,
longest_streak_weeks INTEGER DEFAULT 0,
last_post_date TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
---
12. Mobile-First Design
Same Responsive Strategy as Review Responder
| Breakpoint | Layout | Notes |
|---|---|---|
| `< 640px` (Mobile) | Single column. Full-width cards. Bottom-sticky action buttons. | Primary design target. |
| `640px – 1023px` (Tablet) | Single column with wider cards. | |
| `≥ 1024px` (Desktop) | Fixed sidebar nav. Content area centered. |
Mobile-Specific Post Creation
- Post editor is full-screen on mobile (no distractions)
- Swipe between suggested posts
- Photo upload supports camera capture directly
- Preview shows how the post will look on Google (approximate rendering)
- Quick action: long-press a suggestion to generate and approve in one step
Performance Targets
| Metric | Target |
|---|---|
| First Contentful Paint | < 1.5s on 4G |
| Website scraping completion | < 30s |
| Post generation (streaming) | First token < 2s |
| Image upload | < 5s on 4G |
---
13. Technical Stack
| Component | Technology |
|---|---|
| Frontend | React + TypeScript + Vite + Tailwind CSS |
| UI Components | shadcn/ui |
| Routing | wouter |
| Data Fetching | TanStack React Query v5 |
| Backend | Express.js (TypeScript) |
| Database | PostgreSQL |
| Auth | Email/password + Google OAuth |
| AI | Anthropic Claude (primary) + OpenAI GPT-4o (fallback) |
| Web Scraping | Cheerio + node-fetch (server-side) |
| Google Integration | GBP API v4 + Google OAuth 2.0 |
| Deployment | Replit |
Environment Variables
```
DATABASE_URL=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
ANTHROPIC_API_KEY= (via Replit AI integration)
OPENAI_API_KEY= (via Replit AI integration)
```
---
14. Testing Scenarios
Onboarding & Scraping
| # | Scenario | Expected Result |
|---|---|---|
| T1 | Scrape website with clear service/area pages | Services and service area extracted, tone analyzed |
| T2 | Scrape website with no services page | System extracts what it can from homepage, prompts contractor to add services manually |
| T3 | Website URL is invalid or unreachable | Friendly error, offer manual entry fallback |
| T4 | Contractor has no website | Manual entry form collects services, area, and tone preferences |
| T5 | Fetch old GBP posts via API | Posts analyzed for tone and topic patterns |
| T6 | No old GBP posts exist | System generates posts from website data + default templates |
Post Generation
| # | Scenario | Expected Result |
|---|---|---|
| T7 | Generate "What's New" post | Post is 50-150 words, mentions a specific city, focuses on one service, no banned phrases |
| T8 | Generate "Offer" post | Post includes offer details, expiry, how to claim, casual tone |
| T9 | Generate "Event" post | Post includes event details, date/time, location |
| T10 | Edit post and approve | Edited version saved as training example |
| T11 | Approve without editing | Post saved as approved_as_is example |
| T12 | Regenerate post | New post generated, old draft preserved |
Post Publishing
| # | Scenario | Expected Result |
|---|---|---|
| T13 | Post to GBP via API | Post appears on Google, status → Published, google_post_id stored |
| T14 | Copy post (manual mode) | Text copied to clipboard, status → Published (manual) |
| T15 | Post with image | Image uploaded to GBP along with post text |
| T16 | GBP API failure | Error shown, post saved as draft, retry available |
Post Suggestions
| # | Scenario | Expected Result |
|---|---|---|
| T17 | Services rotated in suggestions | New suggestions avoid recently posted services |
| T18 | Seasonal suggestion in summer | AC/cooling-related topics suggested |
| T19 | Service area rotation | Different cities appear in consecutive suggestions |
| T20 | No posts in 7+ days | Reminder notification sent |
Multi-Shot Learning
| # | Scenario | Expected Result |
|---|---|---|
| T21 | After 5 edited posts, generate new one | Post incorporates patterns from edited examples |
| T22 | Same post type examples selected | "What's New" generation uses "What's New" examples |
Language
| # | Scenario | Expected Result |
|---|---|---|
| T23 | Language set to Spanish | Posts generated in Spanish |
| T24 | Change language in settings | New post drafts use new language |
Edge Cases
| # | Scenario | Expected Result |
|---|---|---|
| T25 | Website has very little content | System uses minimal extraction + prompts for manual input |
| T26 | Contractor has 50+ old GBP posts | System samples most recent 20 for analysis |
| T27 | AI generation fails | Error message, manual textarea editable |
| T28 | Google token expired during post | Token refresh attempted, if fails → manual mode |