The AI Trades
Marketing & Follow-Up

GBP Post Generator from Completed Jobs

No-Code Flow 2 hours
Google Business Profile

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

Input

Completed jobs from past week (type, neighborhood, photos)

Transformation

AI picks 3 best jobs, writes neighborhood-specific posts with CTA, formats photos, queues for approval.

Output

3 fresh GBP posts per week on autopilot. Local SEO boost.

ReplacesSocial media manager ($500-2,000/mo)

Importable Templates

Make.comCreate GBP posts with ChatGPT
Zapier5 ways to automate Google Business Profile
n8nGBP reports with GPT insights

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:

Integration Docs (include with build):

---

## Table of Contents

1. Recipe Overview

2. Strategy Brief

3. Website & GBP Scraping

4. Tone & Service Area Extraction

5. Anti-AI Writing System for Posts

6. Multi-Shot Learning from User Edits

7. Post Creation Flow

8. Post Types & Templates

9. Onboarding Flow

10. Settings & Language

11. Data Model

12. Mobile-First Design

13. Technical Stack

14. Testing Scenarios

---

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 TellExampleWhy 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:

InteractionWhat We StoreHow It Trains
Approve without editingGenerated post + "approved_as_is: true"Good example — add to few-shot pool
Edit then approveAI draft + contractor's edited versionThe EDITED version becomes the few-shot example
RegenerateDraft + regeneration countSignals 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

BreakpointLayoutNotes
`< 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

MetricTarget
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

ComponentTechnology
FrontendReact + TypeScript + Vite + Tailwind CSS
UI Componentsshadcn/ui
Routingwouter
Data FetchingTanStack React Query v5
BackendExpress.js (TypeScript)
DatabasePostgreSQL
AuthEmail/password + Google OAuth
AIAnthropic Claude (primary) + OpenAI GPT-4o (fallback)
Web ScrapingCheerio + node-fetch (server-side)
Google IntegrationGBP API v4 + Google OAuth 2.0
DeploymentReplit

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

#ScenarioExpected Result
T1Scrape website with clear service/area pagesServices and service area extracted, tone analyzed
T2Scrape website with no services pageSystem extracts what it can from homepage, prompts contractor to add services manually
T3Website URL is invalid or unreachableFriendly error, offer manual entry fallback
T4Contractor has no websiteManual entry form collects services, area, and tone preferences
T5Fetch old GBP posts via APIPosts analyzed for tone and topic patterns
T6No old GBP posts existSystem generates posts from website data + default templates

Post Generation

#ScenarioExpected Result
T7Generate "What's New" postPost is 50-150 words, mentions a specific city, focuses on one service, no banned phrases
T8Generate "Offer" postPost includes offer details, expiry, how to claim, casual tone
T9Generate "Event" postPost includes event details, date/time, location
T10Edit post and approveEdited version saved as training example
T11Approve without editingPost saved as approved_as_is example
T12Regenerate postNew post generated, old draft preserved

Post Publishing

#ScenarioExpected Result
T13Post to GBP via APIPost appears on Google, status → Published, google_post_id stored
T14Copy post (manual mode)Text copied to clipboard, status → Published (manual)
T15Post with imageImage uploaded to GBP along with post text
T16GBP API failureError shown, post saved as draft, retry available

Post Suggestions

#ScenarioExpected Result
T17Services rotated in suggestionsNew suggestions avoid recently posted services
T18Seasonal suggestion in summerAC/cooling-related topics suggested
T19Service area rotationDifferent cities appear in consecutive suggestions
T20No posts in 7+ daysReminder notification sent

Multi-Shot Learning

#ScenarioExpected Result
T21After 5 edited posts, generate new onePost incorporates patterns from edited examples
T22Same post type examples selected"What's New" generation uses "What's New" examples

Language

#ScenarioExpected Result
T23Language set to SpanishPosts generated in Spanish
T24Change language in settingsNew post drafts use new language

Edge Cases

#ScenarioExpected Result
T25Website has very little contentSystem uses minimal extraction + prompts for manual input
T26Contractor has 50+ old GBP postsSystem samples most recent 20 for analysis
T27AI generation failsError message, manual textarea editable
T28Google token expired during postToken refresh attempted, if fails → manual mode
RolesMarketingOwner
IndustriesHVACPlumbingElectricalCleaningLandscaping
PrinciplesOne Job, Seven OutputsLook 10x Your Size