惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
V
V2EX
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
小众软件
小众软件
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
The Cloudflare Blog
T
Tailwind CSS Blog
H
Help Net Security
腾讯CDC
爱范儿
爱范儿
人人都是产品经理
人人都是产品经理
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
D
DataBreaches.Net
C
Check Point Blog
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I built automated Google Business Profile(GBP) postin...
Vinit Gohil · 2026-06-19 · via DEV Community

Vinit Gohil

I built a feature recently that I wanted to write up — automated weekly Google Business Profile posts triggered by a cron job. Here is how it works, what the GBP API is actually like, and the gotchas I ran into.


The problem

Local business owners almost never post on Google Business Profile consistently, even though it directly helps local search rankings. The friction is the problem — it is one more task that never gets done.

So I built a system that does it automatically every Monday.


The stack

  • Next.js 14 (App Router)
  • Vercel Cron — triggers the job every Monday at 3:30am UTC
  • Google Business Profile API v4 — publishes the post
  • Prisma + Neon — stores connection tokens and post history
  • Resend — sends the user an email confirmation

Step 1 — Google OAuth setup

The GBP API requires OAuth 2.0 with the business.manage scope. Standard OAuth flow — redirect to Google, exchange code for tokens, store access + refresh tokens.

const GBP_SCOPE = 'https://www.googleapis.com/auth/business.manage'

export function getGbpAuthUrl(state: string): string {
  const params = new URLSearchParams({
    client_id:     process.env.GOOGLE_CLIENT_ID!,
    redirect_uri:  process.env.GBP_REDIRECT_URI!,
    response_type: 'code',
    scope:         GBP_SCOPE,
    access_type:   'offline',
    prompt:        'consent',   // forces refresh_token to be returned
    state,
  })
  return `https://accounts.google.com/o/oauth2/v2/auth?${params}`
}

Gotcha: Without prompt: 'consent', Google only returns a refresh token on the very first authorisation. If the user has authorised before, you get no refresh token and your access token expires in 1 hour with no way to renew it. Always force consent.


Step 2 — Token refresh helper

Access tokens expire after 1 hour. Before every API call, check expiry and refresh if needed.

export async function getValidGbpToken(userId: string): Promise<string | null> {
  const conn = await prisma.gbpConnection.findUnique({ where: { userId } })
  if (!conn) return null

  // Still valid
  if (conn.expiresAt > new Date()) return conn.accessToken

  // Refresh
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id:     process.env.GOOGLE_CLIENT_ID!,
      client_secret: process.env.GOOGLE_CLIENT_SECRET!,
      refresh_token: conn.refreshToken,
      grant_type:    'refresh_token',
    }),
  })

  const data = await res.json()
  if (!data.access_token) return null

  await prisma.gbpConnection.update({
    where: { userId },
    data: {
      accessToken: data.access_token,
      expiresAt:   new Date(Date.now() + data.expires_in * 1000),
    },
  })

  return data.access_token
}


Step 3 — Publishing to GBP

The GBP API endpoint for creating a post:

POST https://mybusiness.googleapis.com/v4/{locationId}/localPosts

export async function createGbpPost(
  accessToken: string,
  locationId: string,   // e.g. "accounts/123/locations/456"
  content: string,
): Promise<{ name: string }> {
  const res = await fetch(
    `https://mybusiness.googleapis.com/v4/${locationId}/localPosts`,
    {
      method: 'POST',
      headers: {
        Authorization:  `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        languageCode: 'en',
        summary:      content,
        topicType:    'STANDARD',
      }),
    }
  )

  if (!res.ok) {
    const err = await res.text()
    throw new Error(`GBP API error ${res.status}: ${err}`)
  }

  return res.json()
}

Gotcha: The GBP API is separate from the Google Maps API and the Places API. The base URL is mybusiness.googleapis.com — not maps.googleapis.com. The documentation mixes these up constantly and it caused me 2 hours of confusion.


Step 4 — The Vercel Cron

// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/gbp-posts",
      "schedule": "30 3 * * 1"
    }
  ]
}

The route handler:

// app/api/cron/gbp-posts/route.ts
export async function GET(req: Request) {
  const cronSecret = process.env.CRON_SECRET
  if (!cronSecret || req.headers.get('authorization') !== `Bearer ${cronSecret}`) {
    return new Response('Unauthorized', { status: 401 })
  }

  const connections = await prisma.gbpConnection.findMany({
    where:  { locationId: { not: null } },
    select: { userId: true },
  })

  const results = await Promise.allSettled(
    connections.map((c) => processGbpPostForUser(c.userId))
  )

  const posted = results.filter(
    (r) => r.status === 'fulfilled' && r.value.success
  ).length

  return Response.json({ processed: results.length, posted })
}

Vercel injects the CRON_SECRET automatically and sends it as a Bearer token on every cron invocation — so the auth check just works.


What I learned

  1. GBP API docs are rough. Half the examples reference deprecated v3 endpoints. Use v4 and expect some trial and error.
  2. Always request offline access and force consent prompt. Otherwise you get a one-hour access token with no refresh capability.
  3. Extracting the location list is separate from posting. You need mybusinessaccountmanagement.googleapis.com to list accounts, mybusinessbusinessinformation.googleapis.com to get location details, and mybusiness.googleapis.com to post. Three different base URLs.
  4. Test with a real GBP account. There is no sandbox. Create a test Google Business Profile for a fake business during development.

This is part of SEO-Snap (https://seo-snap.com) — an SEO audit tool for local businesses. Happy to answer questions on the GBP API — drop them in the comments.