Skip to content

0xdeadd/SEODesk

Repository files navigation

ZanyCuts SEO Desk

Private ChatGPT app for local SEO and ads management - Built for ZanyCuts in Newnan, GA

A production-ready MCP (Model Context Protocol) server that enables non-technical staff to execute local SEO tasks inside ChatGPT. Generate Wix-ready copy, manage Google Business Profile posts and reviews, monitor Core Web Vitals, and track performance—all from one conversational interface.

Features

🎯 Core Flows

  1. Content and GBP Post - Generate SEO-optimized page copy (title tags, meta descriptions, H1s, intros, alt text, internal links) and publish matching Google Business Profile posts
  2. Reviews and Reputation - Fetch GBP reviews and generate three human-sounding reply drafts per review
  3. Speed and Health - Analyze Core Web Vitals (LCP, CLS, INP) with actionable PageSpeed recommendations

📊 Additional Features

  • GA4 Event Tracking - Server-side event tracking via Measurement Protocol
  • Local Intent Detection - Automatically highlights Newnan, GA-focused queries
  • UTM Consistency - Pre-configured UTM parameters for accurate GA4 attribution
  • Family-Friendly Tone - All generated content maintains ZanyCuts' welcoming brand voice

Technology Stack

  • Runtime: Node.js 18+
  • Language: TypeScript (strict mode)
  • Framework: Express
  • APIs: Google Search Console, Business Profile, PageSpeed Insights, GA4 Measurement Protocol
  • Authentication: OAuth 2.0 with refresh tokens

Quick Start

Prerequisites

  • Node.js 18 or higher
  • A Google Cloud Project with required APIs enabled
  • Google OAuth credentials
  • Access to ZanyCuts' Google properties (Search Console, Business Profile, Analytics)

Installation

# Clone or navigate to the project directory
cd /home/clint/projects/SEODesk

# Install dependencies
npm install

# Copy environment template
cp env.example .env

# Edit .env and fill in all values (see OAuth Setup below)
nano .env

# Build the TypeScript project
npm run build

# Start the development server
npm run dev

The server will start on http://localhost:3000 by default.

Test the Server

# Health check
curl http://localhost:3000/health

# Expected response:
# {"status":"healthy","version":"1.0.0","timestamp":"2025-10-09T..."}

OAuth Setup

Step 1: Create Google Cloud Project

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Note your Project ID

Step 2: Enable Required APIs

Enable these APIs in your Google Cloud Project:

  • Google Search Console API
  • Google My Business API (Business Profile)
  • PageSpeed Insights API
  • Google Analytics Data API
# Or enable via gcloud CLI:
gcloud services enable searchconsole.googleapis.com
gcloud services enable mybusiness.googleapis.com
gcloud services enable pagespeedonline.googleapis.com
gcloud services enable analyticsdata.googleapis.com

Step 3: Create OAuth 2.0 Credentials

  1. Go to APIs & Services > Credentials
  2. Click Create Credentials > OAuth client ID
  3. Choose Web application
  4. Add authorized redirect URIs:
    • http://localhost:3000/oauth/callback (for local development)
    • Your production URL (if deploying to Cloud Run)
  5. Click Create
  6. Copy the Client ID and Client Secret
  7. Add them to your .env file:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret

Step 4: Obtain Refresh Token

You need to perform a one-time OAuth flow to get a refresh token:

  1. Install OAuth Playground Helper (or use Google's OAuth Playground):
# Using Google OAuth Playground (easiest method)
# 1. Go to https://developers.google.com/oauthplayground/
# 2. Click the gear icon (⚙️) in the top right
# 3. Check "Use your own OAuth credentials"
# 4. Enter your Client ID and Client Secret
# 5. In Step 1, select these scopes:
#    - https://www.googleapis.com/auth/webmasters.readonly
#    - https://www.googleapis.com/auth/business.manage
# 6. Click "Authorize APIs"
# 7. Sign in with the Google account that has access to ZanyCuts properties
# 8. In Step 2, click "Exchange authorization code for tokens"
# 9. Copy the "Refresh token" value
  1. Add the refresh token to .env:
GOOGLE_REFRESH_TOKEN=1//your-refresh-token-here

Step 5: Configure Property URLs and IDs

Search Console Property URL

Format: sc-domain:yourdomain.com or https://www.yourdomain.com/

# Find your property URL:
# 1. Go to https://search.google.com/search-console
# 2. Select ZanyCuts property
# 3. Copy the URL from the browser or use the exact property name

GSC_PROPERTY_URL=sc-domain:zanycuts.com

Google Business Profile Location Name

Format: accounts/{account_id}/locations/{location_id}

# Find your location name:
# 1. Go to https://business.google.com/
# 2. Select ZanyCuts location
# 3. Look at the URL: business.google.com/u/0/locations/{location_id}
# 4. For account_id, you may need to use the GBP API or contact support
# Or use this helper script:

# Option: Use the GBP API to list locations (after setting up OAuth)
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  https://mybusinessbusinessinformation.googleapis.com/v1/accounts/YOUR_ACCOUNT_ID/locations

GBP_LOCATION_NAME=accounts/123456789/locations/987654321

GA4 Configuration

# Find Measurement ID:
# 1. Go to GA4 Admin > Data Streams
# 2. Select your web stream
# 3. Copy the Measurement ID (format: G-XXXXXXXXXX)

GA4_MEASUREMENT_ID=G-XXXXXXXXXX

# Create API Secret:
# 1. In the same Data Stream settings, scroll to "Measurement Protocol API secrets"
# 2. Click "Create"
# 3. Give it a nickname (e.g., "SEO Desk Server")
# 4. Copy the secret value

GA4_API_SECRET=your-measurement-protocol-secret

Step 6: Verify OAuth Setup

# Start the server
npm run dev

# Test Search Console access
curl http://localhost:3000/get_gsc_queries

# If successful, you'll see query data
# If you see an error, check your OAuth credentials and scopes

API Endpoints

GET /get_gsc_queries

Fetches Search Console queries for the last 28 days with local intent highlighting.

Example:

curl http://localhost:3000/get_gsc_queries

Response:

{
  "queries": [
    {
      "query": "kids haircuts newnan ga",
      "page": "https://zanycuts.com/kids-haircuts",
      "clicks": 45,
      "impressions": 890,
      "ctr": 0.051,
      "position": 3.2,
      "localIntent": true
    }
  ],
  "dateRange": {
    "start": "2025-09-11",
    "end": "2025-10-09"
  }
}

POST /compose_page_copy

Generates SEO-optimized page copy for Wix.

Example:

curl -X POST http://localhost:3000/compose_page_copy \
  -H "Content-Type: application/json" \
  -d '{
    "query": "kids haircuts newnan ga",
    "targetUrl": "https://zanycuts.com/kids-haircuts"
  }'

Response:

{
  "copy": {
    "titleTag": "Kids Haircuts in Newnan, GA | ZanyCuts",
    "metaDescription": "Family-friendly kids haircuts in Newnan, GA with patient stylists and easy online booking...",
    "h1": "Kids Haircuts in Newnan, GA",
    "introParagraph": "Looking for kids haircuts in Newnan, GA? ZanyCuts specializes...",
    "imageAltText": "Child enjoying kids haircuts at ZanyCuts in Newnan GA",
    "internalLinks": [
      { "text": "Kids' Cuts", "url": "/kids-cuts" },
      { "text": "First Haircut Package", "url": "/first-haircut" }
    ],
    "gbpDirectionsUrl": "https://g.page/zanycuts?utm_source=gbp&utm_medium=organic&utm_campaign=local"
  },
  "gbpPostDraft": "New page alert! 🎉 We just updated our Kids Haircuts page..."
}

POST /write_gbp_post

Publishes a post to Google Business Profile.

Example:

curl -X POST http://localhost:3000/write_gbp_post \
  -H "Content-Type: application/json" \
  -d '{
    "message": "New page alert! Check out our updated Kids Haircuts page with all the info about our gentle stylists in Newnan, GA!",
    "ctaUrl": "https://zanycuts.com/kids-haircuts?utm_source=gbp&utm_medium=organic&utm_campaign=local",
    "ctaType": "LEARN_MORE"
  }'

Response:

{
  "success": true,
  "postName": "locations/123456789/localPosts/987654321",
  "publishTime": "2025-10-09T14:30:00Z"
}

GET /fetch_reviews

Fetches latest Google Business Profile reviews with suggested reply drafts.

Example:

curl http://localhost:3000/fetch_reviews?limit=5

Response:

{
  "reviews": [
    {
      "reviewId": "review_123",
      "starRating": 5,
      "comment": "My daughter loved her first haircut here!",
      "reviewer": "Sarah M.",
      "createTime": "2025-10-05T10:20:00Z",
      "hasReply": false,
      "suggestedReplies": [
        {
          "version": 1,
          "text": "Thank you so much, Sarah! We're thrilled your daughter had such a positive first haircut experience..."
        }
      ]
    }
  ]
}

POST /reply_to_review

Posts an owner reply to a Google Business Profile review.

Example:

curl -X POST http://localhost:3000/reply_to_review \
  -H "Content-Type: application/json" \
  -d '{
    "reviewId": "review_123",
    "reply": "Thank you so much, Sarah! We'\''re thrilled your daughter had such a positive experience with us."
  }'

Response:

{
  "success": true,
  "replyTime": "2025-10-09T14:35:00Z"
}

POST /check_pagespeed

Analyzes Core Web Vitals using PageSpeed Insights.

Example:

curl -X POST http://localhost:3000/check_pagespeed \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://zanycuts.com/kids-haircuts",
    "strategy": "mobile"
  }'

Response:

{
  "url": "https://zanycuts.com/kids-haircuts",
  "strategy": "mobile",
  "metrics": {
    "lcp": { "value": 2.3, "score": 0.92, "status": "PASS" },
    "cls": { "value": 0.05, "score": 0.98, "status": "PASS" },
    "inp": { "value": 150, "score": 0.85, "status": "PASS" }
  },
  "recommendations": [
    "Compress hero image (current size: 2.4MB)",
    "Defer non-critical JavaScript"
  ],
  "issuesSummary": "All Core Web Vitals metrics passing. Consider image compression for faster load times."
}

POST /send_ga4_event

Sends a server-side event to GA4 via Measurement Protocol.

Example:

curl -X POST http://localhost:3000/send_ga4_event \
  -H "Content-Type: application/json" \
  -d '{
    "eventName": "content_published",
    "params": {
      "page_type": "service_page",
      "keyword": "kids haircuts newnan ga"
    }
  }'

Response:

{
  "success": true,
  "clientId": "gen_1234567890.1234567890"
}

Local SEO Education

Why Local SEO Matters for ZanyCuts

Local SEO helps ZanyCuts appear in search results when families in Newnan, GA search for kids' haircuts and related services. Here's what you need to know:

1. Page-Per-Service Strategy

Rule: Every service needs its own page with "Newnan, GA" targeting.

Why: Google understands specific pages better than generic "Services" pages.

Required Elements Per Page:

  • Title Tag: Must include service + "Newnan, GA" + "ZanyCuts"
    • Example: Kids Haircuts in Newnan, GA | ZanyCuts
  • Meta Description: Brief description with location and service
  • H1: Service name + "Newnan, GA"
  • First Paragraph: Must mention Newnan, GA and include booking CTA
  • Image Alt Text: Service + "Newnan GA" (natural, not stuffed)

2. Internal Linking

What It Is: Links between your own pages (e.g., Kids' Cuts → First Haircut Package)

Why It Helps:

  • Helps Google understand relationships between services
  • Keeps visitors on your site longer
  • Distributes "page authority" across your site

Best Practice: Link to 5-7 related pages in each service page's content.

3. Google Business Profile (GBP)

Post Freshness:

  • Post at least 1-2 times per week
  • Fresh posts signal to Google that your business is active
  • Use photos when possible (kids getting haircuts, fun moments)

Review Velocity:

  • Encourage happy customers to leave reviews
  • More recent reviews = higher local rankings
  • Respond to ALL reviews (good and bad) within 24-48 hours

Why GBP Posts Matter:

  • They appear in local search results
  • They provide fresh content signals to Google
  • They drive traffic to specific pages with UTM tracking

4. UTM Parameters

What They Are: Tags added to URLs that tell GA4 where traffic came from.

Standard UTM Structure for ZanyCuts:

?utm_source=gbp&utm_medium=organic&utm_campaign=local

Why Consistency Matters:

  • GA4 groups traffic by UTM parameters
  • Inconsistent UTMs = fragmented data
  • Use the EXACT same UTMs across all GBP posts and links

UTM Breakdown:

  • utm_source=gbp - Traffic from Google Business Profile
  • utm_medium=organic - Unpaid (organic) traffic
  • utm_campaign=local - Part of your local SEO campaign

5. Core Web Vitals

What They Are: Google's official page speed metrics that affect rankings.

The Big Three:

  1. LCP (Largest Contentful Paint) - How fast the main content loads
    • Goal: Under 2.5 seconds
  2. CLS (Cumulative Layout Shift) - How much the page "jumps" while loading
    • Goal: Under 0.1
  3. INP (Interaction to Next Paint) - How quickly the page responds to clicks
    • Goal: Under 200ms

Quick Wins:

  • Compress large images (especially hero images)
  • Defer non-critical JavaScript
  • Use web fonts efficiently

Deployment to Cloud Run

Prerequisites

  • Google Cloud SDK installed (gcloud)
  • Docker installed (optional, Cloud Run can build from source)
  • A Google Cloud Project with billing enabled

Deploy Steps

  1. Authenticate with Google Cloud:
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
  1. Build and Deploy:
# From the project root
gcloud run deploy zanycutsdesk \
  --source . \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars NODE_ENV=production
  1. Set Environment Variables:
# Set all required environment variables
gcloud run services update zanycutsdesk \
  --region us-central1 \
  --update-env-vars \
    GOOGLE_CLIENT_ID=your-client-id,\
    GOOGLE_CLIENT_SECRET=your-client-secret,\
    GOOGLE_REFRESH_TOKEN=your-refresh-token,\
    GSC_PROPERTY_URL=sc-domain:zanycuts.com,\
    GBP_LOCATION_NAME=accounts/123/locations/456,\
    GA4_MEASUREMENT_ID=G-XXXXXXXXXX,\
    GA4_API_SECRET=your-api-secret
  1. Get Service URL:
gcloud run services describe zanycutsdesk --region us-central1 --format 'value(status.url)'
  1. Test Deployment:
curl https://zanycutsdesk-xxxxx-uc.a.run.app/health

Production Checklist

  • All environment variables set in Cloud Run
  • OAuth credentials verified (test /get_gsc_queries)
  • Search Console access working
  • GBP post publishing working (test with draft post)
  • GA4 events appearing in DebugView
  • PageSpeed API responding
  • Review fetch working
  • All endpoints returning valid responses
  • HTTPS certificate valid (automatic with Cloud Run)
  • Logs showing no errors in Cloud Logging

Troubleshooting

OAuth Errors

Error: OAUTH_REFRESH_FAILED

Solution:

  1. Verify GOOGLE_REFRESH_TOKEN in .env
  2. Check that refresh token hasn't been revoked (test in OAuth Playground)
  3. Ensure OAuth client credentials match (Client ID and Secret)

Error: GSC_UNAUTHORIZED

Solution:

  1. Verify the Google account used for OAuth has Search Console access
  2. Check that GSC_PROPERTY_URL exactly matches the property in Search Console
  3. Ensure the OAuth scope https://www.googleapis.com/auth/webmasters.readonly was included

GBP Errors

Error: GBP_LOCATION_NOT_FOUND

Solution:

  1. Verify GBP_LOCATION_NAME format: accounts/{account_id}/locations/{location_id}
  2. Test location access via GBP API or Business Profile Manager
  3. Ensure the OAuth account has management access to the location

Rate Limits

Error: RATE_LIMIT_EXCEEDED

Solution:

  • Wait 60 seconds and retry
  • Implement exponential backoff for automated requests
  • Consider caching GSC data for frequently accessed queries

GA4 Events Not Appearing

Issue: Events sent but not showing in GA4 DebugView

Solution:

  1. Verify GA4_MEASUREMENT_ID and GA4_API_SECRET
  2. Use the /send_ga4_event endpoint with a test event
  3. Check GA4 DebugView within 5-10 minutes (events can be delayed)
  4. Ensure event names follow GA4 naming rules (alphanumeric and underscores only)

Development

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

Linting and Formatting

# Run ESLint
npm run lint

# Format code with Prettier
npm run format

Project Structure

src/
├── config/          # Environment configuration
├── oauth/           # OAuth token management
├── clients/         # Google API clients
├── services/        # Business logic (content, replies)
├── routes/          # Express route handlers
├── types/           # TypeScript type definitions
└── index.ts         # Server entry point

Support

For questions or issues, contact the ZanyCuts development team or refer to:


License

Private - ZanyCuts Internal Use Only

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages