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:
1 parent
2145094e55
commit
5bed636cca
6 files changed
+117
-150
No files matched your search
+27
-26
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Site config template — tracked in git, safe to commit (no real secret
|
* 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
|
* 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
|
// 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.
|
// this to the permanent domain once the site moves there.
|
||||||
define('SITE_URL', 'https://theoldfitz.daviddyer.me');
|
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).
|
// The Old Fitz's Google Place ID — identifies the business for the
|
||||||
define('FB_PAGE_ID', '100086635646712');
|
// 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.
|
// API key for the Places API (New), used to fetch the live star rating
|
||||||
// Until this is filled in, the site quietly uses the fallback numbers below.
|
// and review count for the homepage stat card. Until this is filled in,
|
||||||
|
// the site quietly uses the fallback numbers below.
|
||||||
//
|
//
|
||||||
// To generate one:
|
// To generate one:
|
||||||
// 1. Go to https://developers.facebook.com/apps and create an app
|
// 1. Go to https://console.cloud.google.com, create (or pick) a
|
||||||
// (type: "Business"), logged in as an admin of The Old Fitz Page.
|
// project, and enable "Places API (New)" under APIs & Services.
|
||||||
// 2. In Graph API Explorer (developers.facebook.com/tools/explorer),
|
// 2. Enable billing on the project — Google requires a card on file,
|
||||||
// pick that app, select "Page Access Token" for The Old Fitz,
|
// but the free monthly credit comfortably covers a rating/review
|
||||||
// requesting the pages_read_engagement permission.
|
// lookup cached for hours at a time on a low-traffic site like
|
||||||
// 3. Use the "Access Token Debugger" (developers.facebook.com/tools/debug/accesstoken)
|
// this one.
|
||||||
// to extend it to a long-lived token (~60 days). Long-lived Page
|
// 3. Create an API key (APIs & Services → Credentials) and restrict
|
||||||
// tokens generally don't expire on their own with regular use, but
|
// it to "Places API (New)" only.
|
||||||
// Meta can still invalidate one (password change, permission
|
define('GOOGLE_PLACES_API_KEY', '');
|
||||||
// changes, app review) — if that happens, this just falls back to
|
|
||||||
// the numbers below instead of breaking the page.
|
|
||||||
define('FB_PAGE_ACCESS_TOKEN', '');
|
|
||||||
|
|
||||||
// How often to re-fetch from Facebook, in seconds. 6 hours is plenty for
|
// How often to re-fetch from Google, in seconds. 6 hours is plenty for
|
||||||
// numbers that move a few times a month.
|
// a number that moves a few times a month, and stays well inside
|
||||||
define('FB_STATS_CACHE_TTL', 6 * 60 * 60);
|
// 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
|
// Fallback numbers — used whenever there's no API key/Place ID yet, or
|
||||||
// API call fails for any reason. Update these by hand if you're not
|
// the Google API call fails for any reason. Update these by hand if
|
||||||
// using the live token.
|
// you're not using the live lookup.
|
||||||
define('FB_FALLBACK_RECOMMEND_PCT', 92);
|
define('GOOGLE_FALLBACK_RATING', 4.8);
|
||||||
define('FB_FALLBACK_REVIEW_COUNT', 11);
|
define('GOOGLE_FALLBACK_REVIEW_COUNT', 71);
|
||||||
define('FB_FALLBACK_FOLLOWERS', 2600);
|
|
||||||
@@ -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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
require __DIR__ . '/config.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/announcements.php';
|
||||||
require __DIR__ . '/inc/hours.php';
|
require __DIR__ . '/inc/hours.php';
|
||||||
$fbStats = fritz_facebook_stats();
|
$googleReviews = fritz_google_reviews();
|
||||||
$activeAnnouncements = get_active_announcements();
|
$activeAnnouncements = get_active_announcements();
|
||||||
$hoursByDay = get_all_hours();
|
$hoursByDay = get_all_hours();
|
||||||
$siteUrl = rtrim(SITE_URL, '/');
|
$siteUrl = rtrim(SITE_URL, '/');
|
||||||
@@ -125,14 +125,9 @@ $siteUrl = rtrim(SITE_URL, '/');
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-card reveal">
|
<div class="stat-card reveal">
|
||||||
<span class="eyebrow">As the Regulars See It</span>
|
<span class="eyebrow">As the Regulars See It</span>
|
||||||
<div class="stat-big"><?= (int) $fbStats['recommend_pct'] ?>%</div>
|
<div class="stat-big"><?= number_format((float) $googleReviews['rating'], 1) ?>★</div>
|
||||||
<p class="stat-label">recommend The Old Fitz</p>
|
<p class="stat-label">average rating</p>
|
||||||
<p class="stat-source">Source: Facebook reviews (<?= (int) $fbStats['review_count'] ?>)</p>
|
<p class="stat-source">Source: Google reviews (<?= (int) $googleReviews['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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -665,16 +665,6 @@
|
|||||||
color: var(--ink-muted);
|
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 {
|
.stat-source {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|||||||
-10
@@ -655,16 +655,6 @@ section {
|
|||||||
color: var(--ink-muted);
|
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 {
|
.stat-source {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|||||||
Reference in new issue
Block a user