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