Internal Tool

Review Widget Dashboard

Lawn & Land Marketing

Review Widget Dashboard

Add clients, fetch Google reviews, generate embed codes β€” all in one place.

Add Client
πŸ“
πŸ” Don't have the Place ID? Find it here
Search Google for the business β€” click a result to auto-fill the Place ID above.
πŸ“
β€”
β€”
β€”
Rating
β€”
Total Reviews
β€”
5-Star Reviews
β€”
Phone
How It Works
πŸ“‹
Get Embed Code
Copies the HTML snippet you paste onto the client's website. That's all their Google reviews, shown automatically. One snippet per client β€” paste it once and it stays current forever.
πŸ”„
Sync Reviews
Pulls the client's latest Google reviews into our system. Run this when a client gets new reviews and wants the widget updated. Usually takes 20–30 seconds. Happens automatically every 3 days in the background.
β†—
View on Google
Opens their Google Business Profile directly. Use this to verify their listing, check for new reviews, or grab info about the business.
πŸ—‘
Remove
Permanently deletes this client from the system. Their embed widget will stop working immediately. You'll be asked to type the client name to confirm β€” this cannot be undone.

Active Clients

0 clients

No clients yet.
Paste a Place ID above to add your first client.

🐒
Roshi Read First System documentation β€” what this is, how it works, architecture, version history, and roadmap

πŸ”΄ Hard Rule β€” Do Not Modify This Section

This Roshi Read First section is append-only. No content may be removed, consolidated, rewritten, or deleted without Matt Foreman's explicit approval. You may ADD new sections, version entries, and roadmap items freely. You may NOT remove, shorten, or rephrase existing content. If something is outdated, mark it as deprecated β€” don't delete it. This rule applies to Roshi, Kai, and any future agent or developer who touches this file.

🧠 What This Is

Self-hosted Google Review Widget system β€” replaces Elfsight ($5–30+/mo per client that scales with client count). Built by Roshi for Lawn & Land Marketing.

It's an embeddable <script> tag that renders a client's Google reviews as a beautiful card grid on any website. One dashboard manages all ~52 clients. Add a client in 10 seconds, copy the embed code, paste it into their site β€” done.

Why we built it: Elfsight charges per-widget and gets expensive at scale. This is free forever (only cost: Google Places API, which is free up to 1,000 requests/month). Full design control. Faster page loads. And we can revoke it instantly if a client cancels.

πŸ—οΈ Architecture

  • Dashboard: review-widget.lawnlab.dev/[version]/dashboard.html β€” this page. Manages clients, fetches data, generates embed codes.
  • API: /api β€” PHP backend on SiteGround. Handles Google Places API calls, client CRUD, review data serving with CORS headers.
  • Widget: widget.js β€” self-contained embeddable script. Zero dependencies. Fetches reviews JSON, renders card grid, handles themes/colors/responsive layout.
  • Client Data: clients/[slug]/reviews.json + meta.json β€” flat JSON files per client. No database.
  • Config: config.json β€” API key + base URL. API key: Google Places API (New) under project "Lawn and Land Review Widget" (project ID: 368396538141).

Hosting: SiteGround at review-widget.lawnlab.dev. SSH: ssh -i ~/.ssh/lawnlab_roshi -p 18765 u2197-uags5ronvsuk@gtxm1272.siteground.biz. Files at ~/www/review-widget.lawnlab.dev/public_html/.

Root redirect: public_html/index.php forwards to latest version path.

βš™οΈ How It Works

Adding a client (v21+)

One input field handles everything. Type a business name β€” Google results appear in a dropdown showing business name, address, and rating. Click the right one. The system fetches all data and reviews automatically. If you happen to have a Place ID, paste it directly β€” the field detects it and fetches instantly. No modes, no toggles, no instructions needed.

Adding a client (legacy methods)

  • Search by business name + city (uses Google Places Text Search API) β€” OR enter a Place ID directly
  • Dashboard calls /api?action=search β†’ returns matching businesses with Place IDs
  • Click a result β†’ calls /api?action=fetch&placeId=XXX β†’ Google Places API returns business name, rating, total reviews, phone, website, city/state, and up to 5 reviews
  • Choose accent color, theme (light/dark), min star filter β†’ Save
  • API stores meta.json + reviews.json in clients/[slug]/
  • Embed code generated β€” paste into any site's HTML

Adding a Client (v18 β€” deprecated in v20)

Primary method: Place ID. Since the team always has GBP access for every client, the Place ID is the default. Go to business.google.com β†’ select business β†’ Edit profile β†’ Advanced β†’ copy Place ID (starts with ChIJ). Paste it and click Add.

Secondary method: Name search. Available behind the "Search by name instead" link. Useful for sales demos or when GBP access isn't available. May not find very new profiles.

Previous versions (v12-v17) used URL resolution from Google Maps links. This was removed in v18 due to unreliable matching β€” common business names would resolve to the wrong company in a different state.

⚠️ v18 Place ID instructions were incorrect: GBP Advanced Settings shows "Business Profile ID" (a long number), NOT the Place ID (which starts with ChIJ). The team could not follow the instructions because the Place ID is not visible in GBP's interface. This is why v20+ switched to name search as the primary method.

The embed on client sites

  • Script tag loads widget.js from our server
  • Widget fetches reviews via /api?action=reviews&slug=XXX (CORS-enabled PHP endpoint β€” required because SiteGround blocks CORS on static files)
  • Renders card grid: 4 columns desktop β†’ 3 tablet β†’ 2 mobile β†’ 1 phone
  • Shows 2 rows initially. "Show More Reviews" adds 1 row at a time
  • Caps at 20 displayed reviews. After all shown β†’ button becomes "View All Reviews on Google" (opens GBP in new tab)
  • Cards: floating bubbles (20px radius, layered shadows, hover lift). Author name + avatar sit OUTSIDE the card. Google badge on avatar. "Verified Google Review" in italics. Stars are Google yellow (#fbbc04).

Getting embed code for an existing client

"πŸ“‹ Get Code" button on each client card opens a modal with the full embed snippet, pre-configured with that client's accent color, theme, and star filter. Click "Copy Code" to copy to clipboard. This means the team can retrieve any client's embed at any time β€” no need to re-add the client or remember the original code.

Deleting a client

Red "Remove" button on each client card. Triggers a confirmation modal that requires typing the client's exact name to proceed β€” prevents accidental deletions. Once confirmed, deletes their data directory. Widget on their site stops rendering immediately (no reviews.json to fetch = blank).

Auto-refresh (cron job)

Reviews are automatically refreshed every 3 days via cron-refresh.php. The script loops through all clients, hits Google Places API for latest data, and merges new reviews with existing ones (preserving manually-added reviews beyond the API's 5-review limit).

  • Cron URL: https://review-widget.lawnlab.dev/v7/cron-refresh.php?key=llm-refresh-2026
  • Schedule: Every 3 days at 4:00 AM EST (via OpenClaw cron job ID: 56db3595-8215-4659-a9cf-f1882e73b16e)
  • Auth: Requires ?key=llm-refresh-2026 query param or CLI execution. Rejects unauthorized requests.
  • Behavior: Merges, never overwrites. If Roshi manually scraped 15 reviews for a client, the cron won't reduce that to 5 β€” it only adds new ones it finds.
  • Rate limiting: 0.5s delay between clients to avoid Google API throttling.
  • Log: Outputs plain text report β€” can be checked by hitting the URL manually.

The manual "Refresh" button on each client card still works for instant updates.

⚠️ Known Limitations & Critical Notes

  • Google Places API returns max 5 reviews. This is a hard limit across all API tiers. To display more, reviews must be supplemented manually (Roshi scrapes via Camofox) or eventually via Google Business Profile API (OAuth, returns all reviews for managed businesses).
  • L&L Marketing has 11 five-star reviews stored β€” manually scraped from Google Maps. The API only gave us 5 (including 1 one-star). We merged our scraped data with the API business info.
  • SiteGround cache is aggressive. ALWAYS deploy to a new version path (/v1/, /v2/, /v3/). Never edit files in place. Update public_html/index.php redirect after each deploy.
  • CORS: Static .json files on SiteGround do NOT get CORS headers (mod_headers seems disabled or stripped by their proxy). All cross-origin data must be served through /api which sets CORS headers in PHP.
  • WordPress/Avada embed: Use a "Code Block" element (not paragraph/text). Code Block Encoding must be ON in Avada Global Options. Regular content blocks strip <script> tags.
  • Google Cloud project: "Lawn and Land Review Widget" under lawnandlandmarketing.com org. Places API (New) enabled. API key restricted to Places API. Free tier: 1,000 requests/month.
  • GBP Advanced Settings shows "Business Profile ID" β€” NOT Place ID. The numeric ID visible in GBP (e.g. 10180196920814160) is not the same as a Place ID (ChIJ...). The team cannot find Place IDs through GBP's interface. v21 solves this by making name search the only required interaction.

πŸ“‹ Version History

v1 List layout β€” rejected. Matt wanted card grid like Elfsight.
v2 Card grid β€” 4-column grid, orange stars, truncated text + "Read more", avatar with Google badge, light/dark themes.
v3 Floating bubbles β€” author name/avatar moved OUTSIDE the card. 20px border-radius, hover lift, Google yellow stars (#fbbc04), system font stack, CSS 3-line clamp, "Show More" adds 1 row.
v4 Verified badge β€” replaced date with italicized "Verified Google Review".
v5–v6 Dashboard + API β€” admin dashboard, one-field client add via Place ID, PHP API for Google Places, CORS fix (serve JSON through PHP), SiteGround cache busting via version paths.
Dashboard v2 Search + 20-cap β€” business name search via Google Text Search API as primary input. Place ID fallback. Widget caps at 20 five-star reviews, then "View All Reviews on Google" opens GBP in new tab.
Dashboard v3 Roshi Read First β€” added this documentation section. Deployed to review-widget.lawnlab.dev/v3/.
Dashboard v4–v6 CSS fix + hard rule β€” fixed Roshi Read First styles rendering as raw text (CSS was outside style tag). Added append-only hard rule to documentation section.
Dashboard v7 Client cards + safe delete + auto-refresh cron β€” client list redesigned as 3-column card grid (name, city/state, review count, refresh, delete). Delete now requires typing the client's exact name to confirm. Added cron-refresh.php for automated review updates every 3 days.
Dashboard v8 Get Code button β€” each client card now has a "πŸ“‹ Get Code" button that opens a modal with the embed code, ready to copy. Team can retrieve any client's embed code at any time without re-adding the client.
Dashboard v9 Card polish + JS fix + Geocoding API β€” fixed broken JS (template literals containing script tags broke the parser β€” rebuilt modals with DOM methods). Cards now have layered shadows, hover lift animation, rounded 18px corners, divider between location and stats. Service area businesses without a city show "Service Area Business" instead of "Location unknown." Reduced font sizes for sleeker look. Geocoding API enabled (Feb 26, 2026) β€” service area businesses now get proper city/state via reverse geocoding from their viewport center. API key shared with Places API. Previously these showed as "Location Unknown" or relied on phone area code fallback. Now when adding or refreshing a SAB client, the Geocoding API resolves their approximate location automatically.
Dashboard v22 Place ID primary + built-in finder tool β€” Place ID input is front and center (monospace, validates ChIJ). Below it: collapsible "Don't have the Place ID? Find it here" tool. Clicking opens a name search β€” results show business name, address, rating, and truncated Place ID. Clicking a result auto-fills the Place ID input and fetches immediately. This design respects that the team has GBP access (Place ID is the expected input) while providing a built-in lookup when needed. Name search is a TOOL, not the primary flow. Solves the v18 problem (wrong instructions) and v20-21 problem (search front and center despite not working great for all businesses).
Dashboard v21 Single smart input β€” built-in Place ID finder β€” Replaced all previous input modes (Place ID field, name search toggle, help guides) with one unified input. Type a business name β†’ live dropdown shows matches from Google with name, address, and star rating β†’ click to add. Paste a Place ID β†’ auto-detected and fetched instantly. No instructions, no toggles, no modes. The team never needs to know what a Place ID is β€” the system resolves it behind the scenes. This solves the v18 problem where GBP Advanced Settings shows "Business Profile ID" (not Place ID) and the team couldn't follow instructions. Now they just search and click.
Dashboard v20 Name search as default β€” Flipped default to name search with Place ID behind toggle. Intermediate fix before v21's unified approach.
Dashboard v18 Place ID-first redesign β€” Replaced the "smart" unified input with a clean two-mode interface. Default mode is Place ID input (big, obvious, monospace font). Since the team always has GBP access for clients, the Place ID is guaranteed available. Includes step-by-step instructions: business.google.com β†’ Edit profile β†’ Advanced β†’ Place ID. Input validates that the value starts with ChIJ and gives a clear error if not. Name search moved behind "Search by name instead" toggle link (collapsed by default) for future sales demos or non-GBP use. Removed all Google Maps URL resolution code (source of wrong-business bugs). Eliminated CID resolution, ftid resolution, hex ID parsing, and coordinate validation β€” none of it needed when using Place IDs directly.
Dashboard v17 Eliminate wrong-business matches from URL resolution β€” Root cause: Google Maps short links contain a hex ID (internal business identifier) that isn't a valid Google Places API ID. Previous code extracted the business name from the link and searched Google Places globally β€” returning the most prominent business with that name regardless of location. This caused "Forefront, LLC" in Wisconsin to match "ForeFront, LLC." in New Jersey. Fix: when a hex ID is present (meaning the link targets a SPECIFIC business), the system now REFUSES to fall back to global name search. Instead it only tries a strict location-restricted search using coordinates from the link. If the business isn't in Google Places (common for new GBPs), it shows a clear error message explaining the profile isn't indexed yet. Coordinate extraction also improved to handle multiple Google Maps data formats (!8m2!3dLAT!4dLNG, @lat,lng, URL-encoded variants). The v16 distance check is kept as a secondary safety net.
Dashboard v16 Wrong-business detection (coordinate validation) β€” critical fix for URL resolution matching wrong businesses with common names. When a Google Maps short link is resolved, the system now extracts the link's coordinates AND the matched business's coordinates, then compares distance. If >50 miles apart, the preview is BLOCKED with a clear warning: "⚠️ WRONG BUSINESS β€” The link points to a location ~X miles away from [name] in [city]." The user is told to search by exact name + city instead. Also improved error messages for businesses not in Google's Places database β€” shows the extracted name and explains the business isn't indexed yet. This prevents silently loading reviews from the wrong company (e.g. "Forefront, LLC" in NJ when the actual client is in WI).
Dashboard v15 DataForSEO integration β€” all reviews, no limits β€” integrated DataForSEO Google Reviews API to bypass Google Places API's 5-review cap. Each client card now has a ⬇ "Fetch All Reviews" button that triggers a deep fetch via DataForSEO (~$0.02 per client, takes ~20 seconds). Returns up to 50 reviews per business. Cron auto-refresh now runs a DataForSEO deep fetch phase after the standard Google API refresh β€” all clients get full review sets automatically every 3 days. Merge logic preserved: new reviews are added, existing ones never overwritten. DataForSEO credentials stored in config.json alongside Google API key.
Dashboard v14 Maps link resolution fix β€” Google maps.app.goo.gl short links return JavaScript redirects, not HTTP redirects. SiteGround's curl with browser-like headers got a consent page instead of the real Maps preview page. Fix: removed User-Agent/Accept headers from curl, which gets the full 217KB preview page with embedded business data. Added extraction of business name from &q= parameter and coordinates from !3d/!4d data segments in HTML-entity-encoded preview URLs. Location-biased search as fallback for common business names.
Dashboard v12 Smart unified input β€” replaced separate "search by name" and "Place ID" fields with a single smart input that auto-detects what you're typing or pasting. Supports 6 methods: (1) Search by business name + city, (2) Paste a Google Maps share link, (3) Paste a GBP manager URL, (4) GBP Manager "Advanced Info" Place ID, (5) Direct Place ID entry, (6) CID number from Maps URLs. Input shows a live detection badge (e.g. "Place ID", "Maps Link", "Name Search") as you type. Collapsible help guide below the input explains all 6 methods with step-by-step directions for the team. New API endpoints: resolve_cid, resolve_ftid, resolve_url β€” handle conversion of various Google identifiers to Place IDs. Designed for team usability: one field handles everything, no need to understand Place IDs.

πŸ—ΊοΈ Roadmap

  • Review supplementation: Solve the 5-review API limit. Options: (a) Roshi scrapes all reviews per client via Camofox and uploads to JSON, (b) Google Business Profile API with OAuth (returns ALL reviews for managed GBPs β€” best long-term solution, requires OAuth flow setup), (c) DataForSEO Google Reviews endpoint (~$0.02/call).
  • Automated review refresh: Cron job or n8n workflow to periodically re-fetch reviews for all clients. Keeps widget data fresh as new reviews come in.
  • Bulk client import: Pull all ~52 clients from Ground Control or ClickUp, batch-add to widget system.
  • SEO structured data: Add Schema.org LocalBusiness + AggregateRating + individual Review markup to the widget output. Potential for rich snippets in search results.
  • "Fetch All Reviews" button: Dashboard button per client that triggers a deeper scrape beyond the API's 5-review limit.
  • Profile photo support: Google API returns profilePhoto URLs β€” widget currently uses colored initial avatars. Could upgrade to real photos.
  • Analytics: Track widget impressions, "Read more" clicks, "View on Google" clicks per client.

πŸ”§ Deploy Protocol

Every deploy follows this pattern:

  1. Build/edit files locally in /tmp/review-widget/
  2. Create new version directory: mkdir ~/www/review-widget.lawnlab.dev/public_html/vX/clients
  3. SCP all files to new path
  4. Copy client data from previous version: cp -r .../vOLD/clients/* .../vX/clients/
  5. Update config.json with new widgetBaseUrl pointing to /vX
  6. Update root index.php redirect to /vX/dashboard.html
  7. Verify at new URL before sharing

NEVER edit files in an existing version path. SiteGround will serve cached content indefinitely. Always new path.

Client embed codes remain stable as long as widgetBaseUrl in config matches what's in their embed. When moving versions, either keep old paths alive or update embeds.

πŸ“ File Map

  • dashboard.html β€” this page (admin UI)
  • /api β€” backend (search, fetch, save, list, reviews, refresh, delete, scrape)
  • widget.js β€” embeddable review widget (self-contained, no dependencies)
  • config.json β€” API key + widgetBaseUrl
  • demo.html β€” standalone widget demo (light + dark themes)
  • reviews.json β€” L&L's original scraped reviews (legacy, kept for reference)
  • cron-refresh.php β€” auto-refresh script (runs every 3 days, updates all clients from Google)
  • clients/[slug]/reviews.json β€” per-client review data (business info + reviews array)
  • clients/[slug]/meta.json β€” per-client metadata (name, rating, config, timestamps)