[object Object]

← back to Ventura Claw Leads

yolo tick 27: 'More <vertical> nearby' rail at the bottom of every profile

f40cfdad5683ed90472419b0c092a6ddc2d08828 · 2026-05-07 02:39:31 -0700 · Steve Abrams

routes/public.js GET /business/:slug: extra query when the business has
lat/lng — fetches 3 nearest same-vertical active businesses sorted by
squared cartesian distance on lat/lng. Skip the rail entirely when this
business has no coords (rare since seed-geocode runs on every business).

Squared-cartesian is good enough for a 12-mile corridor — proper
Haversine would give true great-circle distance but for a near-flat
slice of LA the absolute error is < 0.01% per pair. Avoids needing the
earthdistance/cube postgres extensions on prod.

views/public/business.ejs: new section after profile-body — H2 'More
<vertical> nearby', sub-text, 3-card grid (mobile collapses to 1 col).
Cards reuse the same hero treatment as /find (photo or vertical
gradient + emoji glyph).

public/css/public.css: .similar-nearby + .similar-grid + responsive
breakpoint at 768.

Live: 'More food & drink nearby' renders on Olive & Salt with 3 nearby
food businesses. Real engagement boost — visitors who land on one
profile via search-engine traffic now have a within-vertical browse path
without backtracking to /find. 46/46 tests still pass.

Files touched

Diff

commit f40cfdad5683ed90472419b0c092a6ddc2d08828
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 02:39:31 2026 -0700

    yolo tick 27: 'More <vertical> nearby' rail at the bottom of every profile
    
    routes/public.js GET /business/:slug: extra query when the business has
    lat/lng — fetches 3 nearest same-vertical active businesses sorted by
    squared cartesian distance on lat/lng. Skip the rail entirely when this
    business has no coords (rare since seed-geocode runs on every business).
    
    Squared-cartesian is good enough for a 12-mile corridor — proper
    Haversine would give true great-circle distance but for a near-flat
    slice of LA the absolute error is < 0.01% per pair. Avoids needing the
    earthdistance/cube postgres extensions on prod.
    
    views/public/business.ejs: new section after profile-body — H2 'More
    <vertical> nearby', sub-text, 3-card grid (mobile collapses to 1 col).
    Cards reuse the same hero treatment as /find (photo or vertical
    gradient + emoji glyph).
    
    public/css/public.css: .similar-nearby + .similar-grid + responsive
    breakpoint at 768.
    
    Live: 'More food & drink nearby' renders on Olive & Salt with 3 nearby
    food businesses. Real engagement boost — visitors who land on one
    profile via search-engine traffic now have a within-vertical browse path
    without backtracking to /find. 46/46 tests still pass.
---
 public/css/public.css     |  8 ++++++++
 routes/public.js          | 20 +++++++++++++++++++-
 views/public/business.ejs | 26 ++++++++++++++++++++++++++
 3 files changed, 53 insertions(+), 1 deletion(-)

diff --git a/public/css/public.css b/public/css/public.css
index 199f398..e2d8c66 100644
--- a/public/css/public.css
+++ b/public/css/public.css
@@ -123,6 +123,14 @@
 .side-card .info-row:last-child { border-bottom: none; }
 .side-card .info-row strong { min-width: 80px; color: var(--fg-muted); font-weight: 500; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
 
+/* Similar-nearby rail */
+.similar-nearby { padding: 32px 28px 48px; max-width: 1100px; margin: 0 auto; border-top: 1px solid var(--border); }
+.similar-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
+.similar-card { padding: 14px 16px; }
+@media (max-width: 768px) {
+  .similar-grid { grid-template-columns: 1fr; }
+}
+
 /* Contact form */
 .contact-form { padding: 18px 20px; border: 2px solid var(--brass); background: var(--bg-alt); border-radius: 8px; }
 .contact-form h3 { margin: 0 0 6px; font-family: var(--serif); font-size: 22px; font-weight: 500; }
diff --git a/routes/public.js b/routes/public.js
index cb14044..fa93a3e 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -188,12 +188,30 @@ router.get('/business/:slug', async (req, res, next) => {
     `, [biz.id]);
     const showActivity = activity.msgs_30d >= 3 || activity.views_30d >= 25;
 
+    // Similar-nearby rail: 3 same-vertical businesses sorted by squared
+    // cartesian lat/lng distance (good enough for a 12-mile corridor; no
+    // PostGIS extension required). Only when this business has coords.
+    let nearby = [];
+    if (biz.latitude != null && biz.longitude != null) {
+      nearby = await db.many(`
+        SELECT slug, business_name, vertical, neighborhood, city, photo_path
+          FROM businesses
+         WHERE id != $1
+           AND vertical = $2
+           AND status = 'active'
+           AND latitude IS NOT NULL AND longitude IS NOT NULL
+         ORDER BY POW(latitude - $3, 2) + POW(longitude - $4, 2) ASC
+         LIMIT 3
+      `, [biz.id, biz.vertical, biz.latitude, biz.longitude]);
+    }
+
     res.render('public/business', {
       title: `${biz.business_name} · ${BY_KEY[biz.vertical]?.label || biz.vertical} on Ventura Blvd`,
       metaDescription: biz.headline || `${biz.business_name} — ${BY_KEY[biz.vertical]?.label || biz.vertical} in ${biz.neighborhood || biz.city || 'the Ventura Blvd corridor'}.`,
       business: biz,
       verticalMeta: BY_KEY[biz.vertical] || null,
-      activity, showActivity
+      activity, showActivity, nearby,
+      verticals: VERTICALS
     });
   } catch (err) { next(err); }
 });
diff --git a/views/public/business.ejs b/views/public/business.ejs
index 3491151..10dab53 100644
--- a/views/public/business.ejs
+++ b/views/public/business.ejs
@@ -200,4 +200,30 @@
   </aside>
 </section>
 
+<% if (typeof nearby !== 'undefined' && nearby && nearby.length > 0) { %>
+  <section class="similar-nearby">
+    <h2 style="font-family:var(--serif);font-weight:500;font-size:24px;margin:0 0 6px">More <%= verticalMeta ? verticalMeta.label.toLowerCase() : '' %> nearby</h2>
+    <p class="muted" style="margin:0 0 18px;font-size:13px">Other Ventura Blvd businesses close to <%= business.business_name %>.</p>
+    <div class="similar-grid">
+      <% nearby.forEach(function(n){
+           var nMeta = verticals.find(function(vv){return vv.key===n.vertical});
+      %>
+        <a class="business-card similar-card" href="/business/<%= n.slug %>">
+          <% if (n.photo_path) { %>
+            <div class="biz-card-hero biz-card-hero-photo">
+              <img src="<%= n.photo_path %>" alt="<%= n.business_name %>" loading="lazy" decoding="async">
+            </div>
+          <% } else { %>
+            <div class="biz-card-hero vertical-hero-<%= n.vertical %>" aria-hidden="true">
+              <span class="biz-card-glyph"><%= nMeta ? nMeta.icon : '•' %></span>
+            </div>
+          <% } %>
+          <p class="biz-name" style="font-size:16px;margin:6px 0 2px"><%= n.business_name %></p>
+          <p class="biz-loc" style="font-size:12px;margin:0"><%= [n.neighborhood, n.city].filter(Boolean).join(' · ') %></p>
+        </a>
+      <% }); %>
+    </div>
+  </section>
+<% } %>
+
 <%- include('../partials/footer') %>

← f41ef50 yolo tick 26: per-neighborhood landing pages — /neighborhood  ·  back to Ventura Claw Leads  ·  yolo tick 28: home 'Recently joined' rail (conditional ≥3 cl 91c32a2 →