← 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
M public/admin/index.htmlM public/index.htmlM server.js
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 →