[object Object]

← back to Ventura Claw Leads

Add 37 vertical+city combo pages to sitemap; unclaimed banner on listing detail

d421ce8fb9d8951048fa82b5e59e4e64cf4afe8c · 2026-05-07 11:55:25 -0700 · Steve Abrams

Two coordinated SEO + monetization improvements:

1. sitemap.xml now lists 37 'vertical in city' combos (8v×5c minus 3
   thin combos with <3 businesses) — '/find?vertical=beauty&city=
   Sherman+Oaks' etc. The /find route already renders unique H1 +
   meta + canonical for each combo (e.g. '155 beauty businesses on
   Ventura Blvd in Sherman Oaks'), so these are real landing pages
   from Google's perspective, not duplicates.

2. /business/<slug> now shows a top-of-page 'unclaimed' banner with
   anchor link to the existing #claim form for the 1,819 corridor-
   seeded shells. The empty 'About' section now reads with context
   ('listed from public records, owners can claim this page free')
   instead of the generic 'this business hasn't added a description'.
   Closes the gap between 'we have 1,819 unclaimed listings' and
   'visitors can find the claim CTA above the fold on mobile'.

Files touched

Diff

commit d421ce8fb9d8951048fa82b5e59e4e64cf4afe8c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 11:55:25 2026 -0700

    Add 37 vertical+city combo pages to sitemap; unclaimed banner on listing detail
    
    Two coordinated SEO + monetization improvements:
    
    1. sitemap.xml now lists 37 'vertical in city' combos (8v×5c minus 3
       thin combos with <3 businesses) — '/find?vertical=beauty&city=
       Sherman+Oaks' etc. The /find route already renders unique H1 +
       meta + canonical for each combo (e.g. '155 beauty businesses on
       Ventura Blvd in Sherman Oaks'), so these are real landing pages
       from Google's perspective, not duplicates.
    
    2. /business/<slug> now shows a top-of-page 'unclaimed' banner with
       anchor link to the existing #claim form for the 1,819 corridor-
       seeded shells. The empty 'About' section now reads with context
       ('listed from public records, owners can claim this page free')
       instead of the generic 'this business hasn't added a description'.
       Closes the gap between 'we have 1,819 unclaimed listings' and
       'visitors can find the claim CTA above the fold on mobile'.
---
 routes/public.js          | 24 +++++++++++++++++++++++-
 views/public/business.ejs | 12 ++++++++++++
 2 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/routes/public.js b/routes/public.js
index 9da3dc9..de8569e 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -601,13 +601,35 @@ router.get('/sitemap.xml', async (req, res, next) => {
       loc: `/neighborhood/${n.slug}`, changefreq: 'daily', priority: '0.7'
     }));
 
+    // 8 verticals × 5 cities = 40 long-tail combo landing pages. The /find
+    // route already supports both filters and renders a unique H1 + meta
+    // for each combo (e.g. "Beauty in Sherman Oaks on Ventura Blvd").
+    // Only include combos that actually have ≥3 businesses — empty results
+    // would be soft-404s in Google's eyes.
+    const comboCounts = await db.many(`
+      SELECT vertical, city, COUNT(*)::int AS n
+        FROM businesses
+       WHERE status = 'active' AND city IS NOT NULL
+       GROUP BY vertical, city
+      HAVING COUNT(*) >= 3
+    `);
+    const verticalCityPages = [];
+    for (const c of comboCounts) {
+      // city goes through encodeURIComponent so 'Sherman Oaks' → 'Sherman%20Oaks'.
+      // Sitemap URLs need to be percent-encoded, then '&' XML-escaped by escape().
+      verticalCityPages.push({
+        loc: `/find?vertical=${c.vertical}&city=${encodeURIComponent(c.city)}`,
+        changefreq: 'weekly', priority: '0.5'
+      });
+    }
+
     // image: namespace declared on <urlset> so Google Image search can ingest
     // <image:image> children for businesses that have a photo on file. Unphotographed
     // listings still appear (just without an <image:image> child) — falling back
     // to the dynamic OG card here would produce thin Image-search results since
     // those aren't real photos of the business.
     let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">\n';
-    for (const p of [...staticPages, ...verticalPages, ...neighborhoodPages]) {
+    for (const p of [...staticPages, ...verticalPages, ...neighborhoodPages, ...verticalCityPages]) {
       xml += `  <url><loc>${baseUrl}${escape(p.loc)}</loc><lastmod>${today}</lastmod><changefreq>${p.changefreq}</changefreq><priority>${p.priority}</priority></url>\n`;
     }
     for (const b of businesses) {
diff --git a/views/public/business.ejs b/views/public/business.ejs
index 7a78159..dba4856 100644
--- a/views/public/business.ejs
+++ b/views/public/business.ejs
@@ -66,6 +66,13 @@
 %>
 <script type="application/ld+json"><%- JSON.stringify(_ldCrumbs) %></script>
 
+<% if (business.claim_status === 'unclaimed') { %>
+  <div role="note" style="background:var(--bg);border-bottom:1px solid var(--border);padding:10px 28px;font-size:13px;text-align:center;color:var(--fg-muted)">
+    <span style="display:inline-block;vertical-align:middle">📋  This listing is unclaimed — sourced from Ventura corridor public records.</span>
+    <a href="#claim" style="display:inline-block;vertical-align:middle;margin-left:6px;font-weight:600;color:var(--brass)">Are you the owner? Claim it free →</a>
+  </div>
+<% } %>
+
 <nav class="breadcrumb" aria-label="Breadcrumb" style="padding:16px 28px 0;max-width:1100px;margin:0 auto;font-size:12px;color:var(--fg-muted);letter-spacing:0.04em">
   <% _crumbs.forEach(function(c, i){
     var isLast = i === _crumbs.length - 1;
@@ -137,6 +144,11 @@
     <% if (business.description) { %>
       <h2>About</h2>
       <p><%= business.description %></p>
+    <% } else if (business.claim_status === 'unclaimed') { %>
+      <h2>About</h2>
+      <p class="muted">This listing is sourced from Ventura corridor public records and hasn't been claimed by the business yet.
+        If you're the owner, you can <a href="#claim">claim this page free</a> to add hours, photos, and a real description.
+      </p>
     <% } else { %>
       <h2>About</h2>
       <p class="muted">This business hasn't added a full description yet. Reach out below — they'll send you details directly.</p>

← 05ae6c7 Add per-vertical sample rails to home page (food/beauty/reta  ·  back to Ventura Claw Leads  ·  Cross-link neighborhood pages to vertical+city combo pages dbc5da9 →