← 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
M agents/animal-yolo/state.jsonM public/css/site.cssM src/server/index.jsM src/server/render.js
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 →