[object Object]

← back to Animals

yolo: Breed page: nearest businesses that work with this breed

446da7c9430bda833f1bbf0180118ea15b50ce10 · 2026-05-13 10:41:22 -0700 · animal-yolo

Task: yolo.breed-page-related-businesses
Prompt: In ~/Projects/animals: /breeds/:slug currently shows top-rated vets generally. Replace with a query that prefers vets in the visitor's home_zip (if logged in) or top-rated for that species. Also add a

Files touched

Diff

commit 446da7c9430bda833f1bbf0180118ea15b50ce10
Author: animal-yolo <steve@designerwallcoverings.com>
Date:   Wed May 13 10:41:22 2026 -0700

    yolo: Breed page: nearest businesses that work with this breed
    
    Task: yolo.breed-page-related-businesses
    Prompt: In ~/Projects/animals: /breeds/:slug currently shows top-rated vets generally. Replace with a query that prefers vets in the visitor's home_zip (if logged in) or top-rated for that species. Also add a
---
 agents/animal-yolo/state.json |  4 ++--
 public/css/site.css           |  4 ++++
 src/server/index.js           |  7 ++++++-
 src/server/render.js          | 10 +++++++++-
 4 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/agents/animal-yolo/state.json b/agents/animal-yolo/state.json
index 3b65682..6d0cb9b 100644
--- a/agents/animal-yolo/state.json
+++ b/agents/animal-yolo/state.json
@@ -1,5 +1,5 @@
 {
-  "cursor": 1,
-  "runs": 105,
+  "cursor": 3,
+  "runs": 107,
   "started_at": "2026-05-08T07:01:22.386Z"
 }
\ No newline at end of file
diff --git a/public/css/site.css b/public/css/site.css
index 765ff65..2c4ed1c 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -50,6 +50,10 @@ img{max-width:100%;display:block}
 .business-list li{padding:.85em 0;border-bottom:1px solid var(--rule);display:flex;flex-wrap:wrap;gap:.75em;align-items:baseline}
 .business-list li small{color:var(--muted)}
 .business-list li .ext{margin-left:auto;font-size:.85em}
+.vet-badge{display:inline-block;font-size:.72em;letter-spacing:.02em;padding:.18em .55em;border-radius:999px;border:1px solid transparent;white-space:nowrap}
+.vet-badge-zip{background:#0f4d3a;color:#fff;border-color:#0f4d3a}
+.vet-badge-zip3{background:#e6efe9;color:#0f4d3a;border-color:#c9d8c1}
+.vet-badge-species{background:#fff7e0;color:#7a5300;border-color:#e8d28a}
 
 .biz-grid{display:grid;grid-template-columns:1fr 320px;gap:2em;margin:1em 0 2em}
 @media (max-width:780px){.biz-grid{grid-template-columns:1fr}.breed-header{grid-template-columns:1fr}.breed-img-large{width:100%;height:200px}}
diff --git a/src/server/index.js b/src/server/index.js
index d1d9fa0..7e14e3c 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -903,8 +903,13 @@ app.get('/breeds/:slug', attachUser, async (req, res) => {
   // preference, not a hard cut. home_zip is null for anonymous visitors → the
   // zip CASE is a no-op and we fall back to species-then-rating ordering.
   const userZip = req.user?.home_zip || null;
+  // matched_* booleans come back so the UI can badge each row with the reason
+  // it ranked — keeps the personalization legible instead of invisible.
   const relatedVets = await many(`
-    SELECT id, name, slug, city, state, zip, latitude, longitude, rating, review_count
+    SELECT id, name, slug, city, state, zip, latitude, longitude, rating, review_count,
+      ($1::text IS NOT NULL AND zip = $1::text)                          AS matched_zip,
+      ($1::text IS NOT NULL AND LEFT(zip,3) = LEFT($1::text,3))          AS matched_zip3,
+      ($2::bigint = ANY(species_served))                                 AS matched_species
     FROM businesses
     WHERE category='vet_clinic' AND opt_out_flag=FALSE
     ORDER BY
diff --git a/src/server/render.js b/src/server/render.js
index cda4bff..c5c700a 100644
--- a/src/server/render.js
+++ b/src/server/render.js
@@ -488,7 +488,15 @@ export function renderBreed({ breed, relatedVets, images = [], ad, dogparkAvatar
     ['Good with kids', breed.good_with_kids ? `${breed.good_with_kids}/5` : null],
     ['Good with pets', breed.good_with_pets ? `${breed.good_with_pets}/5` : null],
   ].filter(([_,v]) => v).map(([k,v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
-  const vets = relatedVets.map(v => `<li><a href="/clinic/${v.id}">${esc(v.name)}</a> <small>${esc(v.city||'')}, ${esc(v.state||'')}${v.rating?` · ★ ${v.rating}`:''}</small></li>`).join('');
+  // Per-row badges explain WHY this vet ranked above the rating-only fallback —
+  // 📍 near you (exact ZIP), ~ near you (same ZIP-3 area), ✓ sees {species}.
+  const vets = relatedVets.map(v => {
+    const badges = [];
+    if (v.matched_zip)         badges.push(`<span class="vet-badge vet-badge-zip" title="In your ZIP">📍 near you</span>`);
+    else if (v.matched_zip3)   badges.push(`<span class="vet-badge vet-badge-zip3" title="Nearby ZIP area">~ near you</span>`);
+    if (v.matched_species)     badges.push(`<span class="vet-badge vet-badge-species" title="Lists ${esc(breed.species)} as a treated species">✓ sees ${esc(breed.species)}s</span>`);
+    return `<li><a href="/clinic/${v.id}">${esc(v.name)}</a> <small>${esc(v.city||'')}, ${esc(v.state||'')}${v.rating?` · ★ ${v.rating}`:''}</small> ${badges.join(' ')}</li>`;
+  }).join('');
   const hero = breed.hero_image_url || images[0]?.image_url || null;
   // Gallery skips the hero (so it doesn't show twice). Per Steve's standing
   // no-stock-images rule, every image is PD/CC0/CC-BY/CC-BY-SA — caption

← 1b82d04 snapshot: 1 file(s) changed, ~1 modified  ·  back to Animals  ·  dog-park: brand logo upper-left, hamburger nav upper-right w 3aa2726 →