[object Object]

← back to Animals

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

e7e8d066566025729a9ee0e05809509490339875 · 2026-05-08 09:04:16 -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 e7e8d066566025729a9ee0e05809509490339875
Author: animal-yolo <steve@designerwallcoverings.com>
Date:   Fri May 8 09:04:16 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 ++--
 src/server/index.js           | 23 +++++++++++++++++------
 src/server/render.js          | 36 ++++++++++++++++++++++++++++++++++--
 3 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/agents/animal-yolo/state.json b/agents/animal-yolo/state.json
index 1b17582..772c950 100644
--- a/agents/animal-yolo/state.json
+++ b/agents/animal-yolo/state.json
@@ -1,5 +1,5 @@
 {
-  "cursor": 2,
-  "runs": 2,
+  "cursor": 3,
+  "runs": 3,
   "started_at": "2026-05-08T07:01:22.386Z"
 }
\ No newline at end of file
diff --git a/src/server/index.js b/src/server/index.js
index 0c65fd6..d4bfc46 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -881,17 +881,28 @@ app.get('/breeds/random', async (req, res) => {
   res.redirect(302, `/breeds/${r.slug}`);
 });
 
-app.get('/breeds/:slug', async (req, res) => {
+app.get('/breeds/:slug', attachUser, async (req, res) => {
   const breed = await one(`
     SELECT b.*, s.name AS species
     FROM breeds b JOIN species s ON s.id=b.species_id
     WHERE b.slug=$1`, [req.params.slug]);
   if (!breed) return res.status(404).send(renderNotFound('Breed not found'));
-  // related vets/groomers — for v1 just nearest top-rated (no geo yet)
+  // Related vets — soft-rank by (a) user's home_zip when logged in, (b) clinics
+  // whose species_served includes this breed's species, then rating/reviews.
+  // No WHERE filter on species_served because it's frequently NULL; this is a
+  // 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;
   const relatedVets = await many(`
-    SELECT id, name, slug, city, state, latitude, longitude, rating
-    FROM businesses WHERE category='vet_clinic' AND opt_out_flag=FALSE
-    ORDER BY rating DESC NULLS LAST, review_count DESC NULLS LAST LIMIT 6`);
+    SELECT id, name, slug, city, state, zip, latitude, longitude, rating, review_count
+    FROM businesses
+    WHERE category='vet_clinic' AND opt_out_flag=FALSE
+    ORDER BY
+      (CASE WHEN $1::text IS NOT NULL AND zip = $1::text THEN 1 ELSE 0 END) DESC,
+      (CASE WHEN $1::text IS NOT NULL AND LEFT(zip,3) = LEFT($1::text,3) THEN 1 ELSE 0 END) DESC,
+      (CASE WHEN $2::bigint = ANY(species_served) THEN 1 ELSE 0 END) DESC,
+      rating DESC NULLS LAST, review_count DESC NULLS LAST
+    LIMIT 6`, [userZip, breed.species_id]);
   // Image gallery from breed_images (Wikimedia Commons w/ full attribution)
   const images = await many(`
     SELECT id, image_url, thumb_url, title, author, author_url,
@@ -910,7 +921,7 @@ app.get('/breeds/:slug', async (req, res) => {
     WHERE da.breed_id = $1 AND bi.dead_at IS NULL
     ORDER BY da.slot ASC`, [breed.id]) : [];
   const ad = await pickAdForPage({ pagePath: req.path, keywords: ['breed', breed.common_name, breed.species], category: null });
-  res.send(renderBreed({ breed, relatedVets, images, ad, dogparkAvatars }));
+  res.send(renderBreed({ breed, relatedVets, images, ad, dogparkAvatars, user: req.user || null }));
 });
 
 // Category directory: /vets, /groomers, /pet-stores, etc.
diff --git a/src/server/render.js b/src/server/render.js
index 3131979..048bc22 100644
--- a/src/server/render.js
+++ b/src/server/render.js
@@ -304,6 +304,36 @@ function leadForm() {
 </section>`;
 }
 
+// Breed-page-only CTA → "Find a {breed} breeder near you". Submits to the same
+// /api/leads endpoint as leadForm() but locks category_wanted='breeder' and
+// species_wanted to this breed's species, and tucks the breed name into the
+// description so the matcher can prioritize breeders that list this breed.
+// ZIP defaults to user.home_zip when logged in (still editable).
+function breederLeadForm(breed, user) {
+  const speciesWanted = (breed.species || '').toLowerCase();   // 'dog','cat',etc.
+  const zipPrefill = (user && /^[0-9]{5}$/.test(user.home_zip || '')) ? user.home_zip : '';
+  return `<section id="breeder-cta" class="lead-form breeder-cta">
+  <h2>Find a ${esc(breed.common_name)} breeder near you</h2>
+  <p class="muted">Tell us your ZIP and we'll match you with up to 3 verified ${esc(breed.common_name)} breeders. Free for you, paid by them.</p>
+  <form method="POST" action="/api/leads" onsubmit="event.preventDefault();const fd=new FormData(this);fetch(this.action,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(fd))}).then(r=>r.json()).then(j=>{this.innerHTML='<p class=\\'ok\\'>Got it! We\\'ll email you 3 ${esc(breed.common_name)} breeder matches within 24h.</p>';});">
+    <input type="hidden" name="category_wanted" value="breeder">
+    <input type="hidden" name="species_wanted" value="${esc(speciesWanted)}">
+    <label>Your name <input name="full_name" required></label>
+    <label>Email <input type="email" name="email" required></label>
+    <label>Phone (optional) <input type="tel" name="phone"></label>
+    <label>ZIP <input name="zip" pattern="[0-9]{5}" required value="${esc(zipPrefill)}"></label>
+    <label>How soon? <select name="urgency">
+      <option value="within_month">This month</option>
+      <option value="within_week">This week</option>
+      <option value="researching">Just researching</option>
+      <option value="immediate">ASAP</option>
+    </select></label>
+    <label>Tell the breeder what you're looking for <textarea name="description" rows="3" placeholder="${esc(breed.common_name)} puppy, male/female, color preference, timing…"></textarea></label>
+    <button type="submit">Match me with ${esc(breed.common_name)} breeders</button>
+  </form>
+</section>`;
+}
+
 export function renderHome({ breeds, counts, categories, myPets = [], galleryStats = null }) {
   // url-slug ('emergency-vets') → db-key ('emergency_vet'); the slugs use plurals,
   // the db uses singular for some. Map explicitly so counts always render.
@@ -446,7 +476,7 @@ export function renderBreedsIndex({ breeds, filters }) {
 </main>` + footer() + '</body></html>';
 }
 
-export function renderBreed({ breed, relatedVets, images = [], ad, dogparkAvatars = [] }) {
+export function renderBreed({ breed, relatedVets, images = [], ad, dogparkAvatars = [], user = null }) {
   const traits = [
     ['Group', breed.group_name], ['Size', breed.size_class],
     ['Weight', breed.weight_lbs_min ? `${breed.weight_lbs_min}–${breed.weight_lbs_max} lb` : null],
@@ -583,8 +613,10 @@ ${ogImage ? `<meta name="twitter:image" content="${esc(ogImage)}">` : ''}
   <section class="breed-gallery">${gallery}</section>
   ${lightboxMarkup()}` : ''}
   ${adSlot(ad, 'native', `/breeds/${breed.slug}`)}
-  <h2>Top-rated vets that see ${esc(breed.common_name)}s</h2>
+  <h2>${user?.home_zip ? `${esc(breed.common_name)} vets near ${esc(user.home_zip)}` : `Top-rated vets that see ${esc(breed.common_name)}s`}</h2>
   <ul class="business-list">${vets || '<li class="muted">No vets seeded yet — coming soon.</li>'}</ul>
+  <p style="margin:1em 0"><a href="#breeder-cta" class="btn-cta" style="display:inline-block;background:#0f4d3a;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;font-weight:600">Find a ${esc(breed.common_name)} breeder near you →</a></p>
+  ${breederLeadForm(breed, user)}
   ${leadForm()}
 </main>` + footer() + '</body></html>';
 }

← 53b3de1 yolo: Surface adoptable animals on pound/shelter detail page  ·  back to Animals  ·  yolo: Email verification on signup 2069600 →