Switch homepage review stat from Facebook to Google

- Replace inc/facebook-stats.php with inc/google-reviews.php: fetches
  average star rating + review count from the Places API (New) instead
  of Facebook's Graph API, same cache-to-JSON/fallback-on-failure shape
  as before.
- index.php: stat card now shows the native star rating ("4.8★ average
  rating") instead of a recommend %, sourced from Google. Drops the
  Facebook-followers line entirely — Google's API has no equivalent and
  showing a stale/static number wasn't worth keeping. "Follow on
  Facebook" links elsewhere on the site are untouched.
- config.example.php: swap FB_PAGE_ID/FB_PAGE_ACCESS_TOKEN/
  FB_STATS_CACHE_TTL/FB_FALLBACK_* for GOOGLE_PLACE_ID/
  GOOGLE_PLACES_API_KEY/GOOGLE_STATS_CACHE_TTL/GOOGLE_FALLBACK_RATING/
  GOOGLE_FALLBACK_REVIEW_COUNT (set to the pub's actual 4.8★/71 reviews),
  with setup instructions in comments. config.php (gitignored) updated
  to match.
- Remove the now-dead .stat-row CSS that only existed for the dropped
  followers line; rebuild styles.css.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
David DyerandClaude Sonnet 5 committed 2026-09-01 18:41:28 -04:00
1 parent 2145094e55
commit 5bed636cca
6 files changed
+117 -150

No files matched your search

+27 -26
View File
@@ -2,7 +2,7 @@
/**
* Site config template — tracked in git, safe to commit (no real secret
* here). Copy this file to config.php (gitignored) and fill in your own
* FB_PAGE_ACCESS_TOKEN there.
* GOOGLE_PLACES_API_KEY there.
*/
// Base URL the site is currently reachable at. Used for social sharing
@@ -11,33 +11,34 @@
// this to the permanent domain once the site moves there.
define('SITE_URL', 'https://theoldfitz.daviddyer.me');
// The Old Fitz's Facebook Page ID (from the facebook.com/profile.php?id=... links already on the site).
define('FB_PAGE_ID', '100086635646712');
// The Old Fitz's Google Place ID — identifies the business for the
// Places API. Look it up with Google's Place ID Finder:
// https://developers.google.com/maps/documentation/places/web-service/place-id
// (search "The Old Fitz Public House, Marion, IN" and copy the ID shown).
define('GOOGLE_PLACE_ID', '');
// Paste a Page Access Token here to make the homepage stats live.
// Until this is filled in, the site quietly uses the fallback numbers below.
// API key for the Places API (New), used to fetch the live star rating
// and review count for the homepage stat card. Until this is filled in,
// the site quietly uses the fallback numbers below.
//
// To generate one:
// 1. Go to https://developers.facebook.com/apps and create an app
// (type: "Business"), logged in as an admin of The Old Fitz Page.
// 2. In Graph API Explorer (developers.facebook.com/tools/explorer),
// pick that app, select "Page Access Token" for The Old Fitz,
// requesting the pages_read_engagement permission.
// 3. Use the "Access Token Debugger" (developers.facebook.com/tools/debug/accesstoken)
// to extend it to a long-lived token (~60 days). Long-lived Page
// tokens generally don't expire on their own with regular use, but
// Meta can still invalidate one (password change, permission
// changes, app review) — if that happens, this just falls back to
// the numbers below instead of breaking the page.
define('FB_PAGE_ACCESS_TOKEN', '');
// 1. Go to https://console.cloud.google.com, create (or pick) a
// project, and enable "Places API (New)" under APIs & Services.
// 2. Enable billing on the project — Google requires a card on file,
// but the free monthly credit comfortably covers a rating/review
// lookup cached for hours at a time on a low-traffic site like
// this one.
// 3. Create an API key (APIs & Services → Credentials) and restrict
// it to "Places API (New)" only.
define('GOOGLE_PLACES_API_KEY', '');
// How often to re-fetch from Facebook, in seconds. 6 hours is plenty for
// numbers that move a few times a month.
define('FB_STATS_CACHE_TTL', 6 * 60 * 60);
// How often to re-fetch from Google, in seconds. 6 hours is plenty for
// a number that moves a few times a month, and stays well inside
// Google's caching allowance for Place Details data.
define('GOOGLE_STATS_CACHE_TTL', 6 * 60 * 60);
// Fallback numbers — used whenever there's no token yet, or the Facebook
// API call fails for any reason. Update these by hand if you're not
// using the live token.
define('FB_FALLBACK_RECOMMEND_PCT', 92);
define('FB_FALLBACK_REVIEW_COUNT', 11);
define('FB_FALLBACK_FOLLOWERS', 2600);
// Fallback numbers — used whenever there's no API key/Place ID yet, or
// the Google API call fails for any reason. Update these by hand if
// you're not using the live lookup.
define('GOOGLE_FALLBACK_RATING', 4.8);
define('GOOGLE_FALLBACK_REVIEW_COUNT', 71);
-94
View File
@@ -1,94 +0,0 @@
<?php
/**
* Pulls "recommend %", review count, and follower count from The Old
* Fitz's Facebook Page via the Graph API, cached to a local JSON file so
* we don't hit Facebook on every page load. Falls back to the numbers in
* config.php whenever there's no access token yet, or the API call fails
* for any reason — the homepage never breaks because of this.
*
* Known limitation: Facebook's newer thumbs-up/down "recommendation"
* system (the "X% recommend (Y reviews)" wording used on the Page today)
* doesn't have a confirmed, stable Graph API field the way star ratings
* do. This code requests overall_star_rating/rating_count and converts
* a star rating to a percentage if Facebook returns one; if the Page's
* review data isn't exposed that way, recommend_pct/review_count will
* silently stay at the config.php fallback while followers_count still
* goes live. That's expected, not a bug — Meta's review-related API
* access has gotten more restricted over time.
*/
function fritz_facebook_stats(): array {
$fallback = [
'recommend_pct' => FB_FALLBACK_RECOMMEND_PCT,
'review_count' => FB_FALLBACK_REVIEW_COUNT,
'followers' => FB_FALLBACK_FOLLOWERS,
'source' => 'fallback',
'fetched_at' => null,
];
if (FB_PAGE_ACCESS_TOKEN === '') {
return $fallback;
}
$cacheFile = __DIR__ . '/../cache/facebook-stats.json';
if (is_file($cacheFile)) {
$cached = json_decode((string) file_get_contents($cacheFile), true);
if (is_array($cached) && (time() - ($cached['fetched_at'] ?? 0)) < FB_STATS_CACHE_TTL) {
$cached['source'] = 'cache';
return $cached;
}
}
$fresh = fritz_fetch_facebook_stats($fallback);
$fresh['fetched_at'] = time();
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0775, true);
}
@file_put_contents($cacheFile, json_encode($fresh));
return $fresh;
}
function fritz_fetch_facebook_stats(array $fallback): array {
if (!function_exists('curl_init')) {
return $fallback;
}
$url = sprintf(
'https://graph.facebook.com/v21.0/%s?fields=followers_count,overall_star_rating,rating_count&access_token=%s',
FB_PAGE_ID,
urlencode(FB_PAGE_ACCESS_TOKEN)
);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $httpCode !== 200) {
return $fallback;
}
$data = json_decode($body, true);
if (!is_array($data) || isset($data['error'])) {
return $fallback;
}
$recommendPct = isset($data['overall_star_rating'])
? (int) round(($data['overall_star_rating'] / 5) * 100)
: $fallback['recommend_pct'];
return [
'recommend_pct' => $recommendPct,
'review_count' => $data['rating_count'] ?? $fallback['review_count'],
'followers' => $data['followers_count'] ?? $fallback['followers'],
'source' => 'live',
];
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* Pulls the average star rating and review count from The Old Fitz's
* Google Business Profile via the Places API (New), cached to a local
* JSON file so we don't hit Google on every page load. Falls back to
* the numbers in config.php whenever there's no API key/Place ID yet,
* or the API call fails for any reason — the homepage never breaks
* because of this.
*/
function fritz_google_reviews(): array {
$fallback = [
'rating' => GOOGLE_FALLBACK_RATING,
'review_count' => GOOGLE_FALLBACK_REVIEW_COUNT,
'source' => 'fallback',
'fetched_at' => null,
];
if (GOOGLE_PLACES_API_KEY === '' || GOOGLE_PLACE_ID === '') {
return $fallback;
}
$cacheFile = __DIR__ . '/../cache/google-reviews.json';
if (is_file($cacheFile)) {
$cached = json_decode((string) file_get_contents($cacheFile), true);
if (is_array($cached) && (time() - ($cached['fetched_at'] ?? 0)) < GOOGLE_STATS_CACHE_TTL) {
$cached['source'] = 'cache';
return $cached;
}
}
$fresh = fritz_fetch_google_reviews($fallback);
$fresh['fetched_at'] = time();
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0775, true);
}
@file_put_contents($cacheFile, json_encode($fresh));
return $fresh;
}
function fritz_fetch_google_reviews(array $fallback): array {
if (!function_exists('curl_init')) {
return $fallback;
}
$url = sprintf(
'https://places.googleapis.com/v1/places/%s',
rawurlencode(GOOGLE_PLACE_ID)
);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'X-Goog-Api-Key: ' . GOOGLE_PLACES_API_KEY,
// Field mask is required by the New Places API (400 without
// it) and also determines the billing SKU — rating and
// userRatingCount are "Place Details (New) - Atmosphere".
'X-Goog-FieldMask: rating,userRatingCount',
],
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $httpCode !== 200) {
return $fallback;
}
$data = json_decode($body, true);
if (!is_array($data) || isset($data['error'])) {
return $fallback;
}
return [
'rating' => $data['rating'] ?? $fallback['rating'],
'review_count' => $data['userRatingCount'] ?? $fallback['review_count'],
'source' => 'live',
];
}
+5 -10
View File
@@ -1,9 +1,9 @@
<?php
require __DIR__ . '/config.php';
require __DIR__ . '/inc/facebook-stats.php';
require __DIR__ . '/inc/google-reviews.php';
require __DIR__ . '/inc/announcements.php';
require __DIR__ . '/inc/hours.php';
$fbStats = fritz_facebook_stats();
$googleReviews = fritz_google_reviews();
$activeAnnouncements = get_active_announcements();
$hoursByDay = get_all_hours();
$siteUrl = rtrim(SITE_URL, '/');
@@ -125,14 +125,9 @@ $siteUrl = rtrim(SITE_URL, '/');
</div>
<div class="stat-card reveal">
<span class="eyebrow">As the Regulars See It</span>
<div class="stat-big"><?= (int) $fbStats['recommend_pct'] ?>%</div>
<p class="stat-label">recommend The Old Fitz</p>
<p class="stat-source">Source: Facebook reviews (<?= (int) $fbStats['review_count'] ?>)</p>
<div class="stat-row">
<div class="stat-big"><?= number_format((int) $fbStats['followers']) ?>+</div>
<p class="stat-label">people following along on Facebook</p>
<p class="stat-source">Source: Facebook followers</p>
</div>
<div class="stat-big"><?= number_format((float) $googleReviews['rating'], 1) ?>★</div>
<p class="stat-label">average rating</p>
<p class="stat-source">Source: Google reviews (<?= (int) $googleReviews['review_count'] ?>)</p>
</div>
</div>
</div>
-10
View File
@@ -665,16 +665,6 @@
color: var(--ink-muted);
}
.stat-card .stat-row {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
}
.stat-card .stat-row .stat-big {
font-size: 1.9rem;
}
.stat-source {
margin-top: 1.5rem;
font-family: var(--font-mono);
-10
View File
@@ -655,16 +655,6 @@ section {
color: var(--ink-muted);
}
.stat-card .stat-row {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
}
.stat-card .stat-row .stat-big {
font-size: 1.9rem;
}
.stat-source {
margin-top: 1.5rem;
font-family: var(--font-mono);