#!/usr/bin/env node /** * Google Reviews Sync Script * * Fetches reviews from Google Places API (New) and saves them as reviews.json. * Intended to run as a daily cron job on the server. * * Environment: * GOOGLE_API_KEY — Google API key with Places API enabled * PLACE_ID — Google Place ID for the business * OUTPUT_PATH — Where to write reviews.json (defaults to /opt/00-infrastructure/Website/website/dist/reviews.json) * * Usage: * GOOGLE_API_KEY=xxx PLACE_ID=yyy node ~/sync-reviews.mjs * * Cron (daily at 6am): * 0 6 * * * GOOGLE_API_KEY=AIzaSyByehddoo7TNSyV6qjmyw2vuMmizIlvqzM PLACE_ID=ChIJ0Xh19SlCkggRGO-WYbMbPgY node ~/sync-reviews.mjs >> /var/log/sync-reviews.log 2>&1 */ import fs from 'fs'; import path from 'path'; const API_KEY = process.env.GOOGLE_API_KEY || 'AIzaSyByehddoo7TNSyV6qjmyw2vuMmizIlvqzM'; const PLACE_ID = process.env.PLACE_ID || 'ChIJ0Xh19SlCkggRGO-WYbMbPgY'; const OUTPUT_PATH = process.env.OUTPUT_PATH || '/opt/00-infrastructure/Website/website/dist/reviews.json'; async function fetchReviews() { // Use Places API (New) — places.googleapis.com const url = `https://places.googleapis.com/v1/places/${PLACE_ID}?fields=reviews&key=${API_KEY}&languageCode=en`; const res = await fetch(url, { headers: { 'X-Goog-FieldMask': 'reviews', }, }); if (!res.ok) { // Fallback to legacy Places API console.log('New API failed, trying legacy Places API...'); return fetchReviewsLegacy(); } const data = await res.json(); return (data.reviews || []).map((r) => ({ author: r.authorAttribution?.displayName || 'Anonymous', rating: r.rating || 5, text: r.text?.text || '', originalText: r.originalText?.text || '', originalLanguage: r.originalText?.languageCode || 'en', date: r.relativePublishTimeDescription || '', profilePhoto: r.authorAttribution?.photoUri || '', })); } async function fetchReviewsLegacy() { const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${PLACE_ID}&fields=reviews&key=${API_KEY}&reviews_sort=newest`; const res = await fetch(url); if (!res.ok) { throw new Error(`Legacy Places API error: ${res.status} ${res.statusText}`); } const data = await res.json(); if (data.status !== 'OK') { throw new Error(`Places API status: ${data.status} — ${data.error_message || ''}`); } return (data.result?.reviews || []).map((r) => ({ author: r.author_name || 'Anonymous', rating: r.rating || 5, text: r.text || '', originalText: r.original_language !== 'en' ? r.text : '', originalLanguage: r.language || 'en', date: r.relative_time_description || '', profilePhoto: r.profile_photo_url || '', })); } async function translateIfNeeded(reviews) { // Google Translate via free translate endpoint const toTranslate = reviews.filter( (r) => r.originalLanguage && r.originalLanguage !== 'en' && r.text ); if (toTranslate.length === 0) return reviews; for (const review of toTranslate) { try { const url = `https://translation.googleapis.com/language/translate/v2?key=${API_KEY}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ q: review.text, source: review.originalLanguage, target: 'en', format: 'text', }), }); if (res.ok) { const data = await res.json(); const translated = data.data?.translations?.[0]?.translatedText; if (translated) { review.originalText = review.text; review.text = translated; } } } catch { // Keep original text if translation fails } } return reviews; } async function main() { console.log(`[${new Date().toISOString()}] Syncing Google reviews...`); console.log(`Place ID: ${PLACE_ID}`); console.log(`Output: ${OUTPUT_PATH}`); try { let reviews = await fetchReviews(); console.log(`Fetched ${reviews.length} reviews`); // Filter out reviews without text reviews = reviews.filter((r) => r.text && r.text.trim().length > 0); console.log(`${reviews.length} reviews have text`); reviews = await translateIfNeeded(reviews); // Ensure output directory exists const dir = path.dirname(OUTPUT_PATH); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(OUTPUT_PATH, JSON.stringify(reviews, null, 2)); console.log(`Saved ${reviews.length} reviews to ${OUTPUT_PATH}`); } catch (err) { console.error('Error syncing reviews:', err.message); process.exit(1); } } main();