[object Object]

← back to Prestige Car Wash

Show real Google photos: server-side photo proxy (/api/places/photo/:i, key kept server-side, cached) + photo gallery on home + photos & reviews in admin Google tab

c69cb231897f8351a1d6f9243ac8f7114b03dc66 · 2026-07-05 17:03:45 -0700 · Steve

Files touched

Diff

commit c69cb231897f8351a1d6f9243ac8f7114b03dc66
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 5 17:03:45 2026 -0700

    Show real Google photos: server-side photo proxy (/api/places/photo/:i, key kept server-side, cached) + photo gallery on home + photos & reviews in admin Google tab
---
 public/admin/index.html |  4 ++++
 public/index.html       |  5 +++++
 server.js               | 16 ++++++++++++++++
 3 files changed, 25 insertions(+)

diff --git a/public/admin/index.html b/public/admin/index.html
index 706cf89..a5aeb48 100644
--- a/public/admin/index.html
+++ b/public/admin/index.html
@@ -188,6 +188,10 @@ function renderGoogle(){
         <div class="blurb">${enc(p.address)} ${p.rating?'· '+rating(p.rating)+' ('+(p.reviews_count||0)+')':''}</div>
         <div class="ttl" style="font-size:14px;margin-top:10px">Hours</div>
         ${(p.hours||[]).map(h=>`<div class="kv"><span>${enc(h)}</span></div>`).join('')}
+        ${(p.photos&&p.photos.length)?`<div class="ttl" style="font-size:14px;margin-top:14px">Photos (${p.photos.length})</div>
+          <div class="grid" style="--cols:3;gap:8px;margin-top:8px">${p.photos.map((_,i)=>`<img src="/api/places/photo/${i}?w=500" loading="lazy" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:10px;border:1px solid var(--line)">`).join('')}</div>`:''}
+        ${(p.reviews&&p.reviews.length)?`<div class="ttl" style="font-size:14px;margin-top:14px">Recent reviews</div>
+          ${p.reviews.slice(0,4).map(r=>`<div class="kv" style="flex-direction:column;align-items:flex-start;gap:2px"><span class="rate">★ ${r.rating} — ${enc(r.author)}</span><span style="color:var(--mut);font-size:12px">${enc((r.text||'').slice(0,180))}${(r.text||'').length>180?'…':''}</span></div>`).join('')}`:''}
         <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
           <button class="btn ghost" onclick="draftUpdate('hours')">✎ Update hours in GBP</button>
           <button class="btn ghost" onclick="draftUpdate('photos')">✎ Add photos in GBP</button>
diff --git a/public/index.html b/public/index.html
index aa87c08..eb0e421 100644
--- a/public/index.html
+++ b/public/index.html
@@ -75,6 +75,7 @@
       <a class="btn" href="/contact" style="margin-top:8px">Book a wash</a>
     </div></div>
   </div>
+  <div class="grid" id="locPhotos" style="--cols:6;margin-top:16px"></div>
 </div></section>
 
 <footer class="foot"><div class="wrap">
@@ -114,6 +115,10 @@ fetch('/api/places').then(r=>r.json()).then(p=>{
   document.getElementById('locAddr').textContent=(p.address||'San Fernando Valley, CA')+(p.phone?' · '+p.phone:'');
   document.getElementById('locHours').innerHTML=(p.hours||[]).map(h=>
     `<div style="padding:5px 0;border-top:1px solid var(--line);font-size:13px;color:var(--mut)">${h}</div>`).join('');
+  // Real Google photos via the server-side proxy (key stays server-side).
+  const n=(p.photos||[]).length;
+  if(n) document.getElementById('locPhotos').innerHTML=Array.from({length:n},(_, i)=>
+    `<img src="/api/places/photo/${i}?w=400" loading="lazy" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:12px;border:1px solid var(--line)">`).join('');
 });
 </script>
 </body>
diff --git a/server.js b/server.js
index 8d4474c..9f30717 100644
--- a/server.js
+++ b/server.js
@@ -216,6 +216,22 @@ app.get('/api/admin/leads', (req, res) => {
 });
 
 // ---- static + clean URLs ---------------------------------------------------
+// Google Place photo proxy — fetches photo media with the server-side key and streams
+// it, so the key never reaches the browser. Cached to avoid re-billing on every view.
+app.get('/api/places/photo/:i', async (req, res) => {
+  const key = process.env.GOOGLE_PLACES_API_KEY;
+  const name = (places().photos || [])[parseInt(req.params.i, 10)];
+  if (!key || !name) return res.status(404).end();
+  const w = Math.min(1600, Math.max(100, parseInt(req.query.w, 10) || 800));
+  try {
+    const r = await fetch(`https://places.googleapis.com/v1/${name}/media?maxWidthPx=${w}&key=${key}`);
+    if (!r.ok) return res.status(502).end();
+    res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg');
+    res.set('Cache-Control', 'public, max-age=86400');
+    res.end(Buffer.from(await r.arrayBuffer()));
+  } catch { res.status(502).end(); }
+});
+
 // Inline SVG favicon (avoids a 404 on the browser's automatic request).
 app.get('/favicon.svg', (req, res) => res.type('image/svg+xml').send(
   '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0a84ff"/><text x="16" y="22" font-size="18" text-anchor="middle">🚗</text></svg>'));

← c09753a Wire Google Places: real listing (7601 Reseda Blvd, 4.3★/47,  ·  back to Prestige Car Wash  ·  Render REAL business photos + videos (do NOT overwrite the c 4e71985 →