[object Object]

← back to Ventura Claw Leads

Sitemap covers paginated pages; head supports rel=prev/next + filtered canonicals; map-CTA on home + find

5cc49d8833053a1376f95aa8f431faf094eb4edc · 2026-05-07 10:35:53 -0700 · Steve Abrams

Three SEO improvements stacked together since they're a single user
journey:

1. sitemap.xml now lists /find?vertical=X&page=2..N for each paginated
   vertical (beauty 4p, food 3p, retail 2p, creative 2p) — 7 new URLs
   so Google crawls every business via the listing pages, not just the
   1,819 direct /business/<slug>.

2. head.ejs gains rel='prev'/rel='next' link tags + a canonicalPath
   override. The /find route now passes the full filter+page query
   string as canonical — /find?vertical=beauty&page=2 self-canonicalizes
   instead of folding into /find.

3. Home + /find both get a 'browse the map →' CTA. Map already accepted
   ?vertical= filter; the /find→/map link preserves it so a user
   filtering for fitness on the list view sees only fitness pins on the
   map. 1,719 markers were unreachable from the home hero before this.

Files touched

Diff

commit 5cc49d8833053a1376f95aa8f431faf094eb4edc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 10:35:53 2026 -0700

    Sitemap covers paginated pages; head supports rel=prev/next + filtered canonicals; map-CTA on home + find
    
    Three SEO improvements stacked together since they're a single user
    journey:
    
    1. sitemap.xml now lists /find?vertical=X&page=2..N for each paginated
       vertical (beauty 4p, food 3p, retail 2p, creative 2p) — 7 new URLs
       so Google crawls every business via the listing pages, not just the
       1,819 direct /business/<slug>.
    
    2. head.ejs gains rel='prev'/rel='next' link tags + a canonicalPath
       override. The /find route now passes the full filter+page query
       string as canonical — /find?vertical=beauty&page=2 self-canonicalizes
       instead of folding into /find.
    
    3. Home + /find both get a 'browse the map →' CTA. Map already accepted
       ?vertical= filter; the /find→/map link preserves it so a user
       filtering for fitness on the list view sees only fitness pins on the
       map. 1,719 markers were unreachable from the home hero before this.
---
 routes/public.js        | 45 +++++++++++++++++++++++++++++++++++++++++----
 views/partials/head.ejs | 15 ++++++++++++++-
 views/public/find.ejs   |  3 +++
 views/public/home.ejs   |  3 +++
 4 files changed, 61 insertions(+), 5 deletions(-)

diff --git a/routes/public.js b/routes/public.js
index 9e6eea5..c6a82a7 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -145,13 +145,35 @@ router.get('/find', async (req, res, next) => {
       : 'Search Ventura Blvd';
 
     const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
+
+    // Build absolute prev/next URLs for the head <link rel> tags. SEO signal
+    // for paginated content — Google uses these to understand the series.
+    const baseUrl = (process.env.PUBLIC_URL || 'https://leads.venturaclaw.com').replace(/\/+$/, '');
+    const buildPageUrl = (n) => {
+      const qsParts = [];
+      if (q) qsParts.push(`q=${encodeURIComponent(q)}`);
+      if (v) qsParts.push(`vertical=${encodeURIComponent(v)}`);
+      if (city) qsParts.push(`city=${encodeURIComponent(city)}`);
+      if (sortKey && orderClauses[sortKey] && sortKey !== 'featured') qsParts.push(`sort=${encodeURIComponent(sortKey)}`);
+      if (n > 1) qsParts.push(`page=${n}`);
+      return baseUrl + '/find' + (qsParts.length ? '?' + qsParts.join('&') : '');
+    };
+    const prevUrl = page > 1 ? buildPageUrl(page - 1) : null;
+    const nextUrl = page < totalPages ? buildPageUrl(page + 1) : null;
+
+    // Canonical for /find: include the meaningful filter params so Google
+    // indexes /find?vertical=beauty&page=2 as its own page, not as a dup of
+    // /find. Use the same buildPageUrl helper to ensure consistent ordering.
+    const canonicalPath = buildPageUrl(page).replace(baseUrl, '');
+
     res.render('public/find', {
       title: customTitle,
       metaDescription: customMeta,
       businesses, totalCount, q, v, city, verticals: VERTICALS,
       sort: orderClauses[sortKey] ? sortKey : 'featured',
       customH1, verticalLabel,
-      page, totalPages, pageSize: PAGE_SIZE
+      page, totalPages, pageSize: PAGE_SIZE,
+      prevUrl, nextUrl, canonicalPath
     });
   } catch (err) { next(err); }
 });
@@ -529,9 +551,24 @@ router.get('/sitemap.xml', async (req, res, next) => {
       { loc: '/terms',   changefreq: 'yearly',  priority: '0.3' }
     ];
     // One <url> per vertical filter — useful long-tail landing pages.
-    const verticalPages = VERTICALS.map(v => ({
-      loc: `/find?vertical=${v.key}`, changefreq: 'daily', priority: '0.7'
-    }));
+    // Plus page=2..N for any vertical whose total >200 (mirrors the /find
+    // pagination), so Google can crawl every business through the listing
+    // pages and not rely solely on direct /business/<slug> URLs.
+    const verticalCounts = await db.many(`
+      SELECT vertical, COUNT(*)::int AS n
+        FROM businesses WHERE status = 'active'
+       GROUP BY vertical
+    `);
+    const PAGE_SIZE = 200;
+    const verticalCountMap = Object.fromEntries(verticalCounts.map(r => [r.vertical, r.n]));
+    const verticalPages = [];
+    for (const v of VERTICALS) {
+      verticalPages.push({ loc: `/find?vertical=${v.key}`, changefreq: 'daily', priority: '0.7' });
+      const totalPages = Math.ceil((verticalCountMap[v.key] || 0) / PAGE_SIZE);
+      for (let p = 2; p <= totalPages; p++) {
+        verticalPages.push({ loc: `/find?vertical=${v.key}&page=${p}`, changefreq: 'daily', priority: '0.5' });
+      }
+    }
     // One <url> per neighborhood landing page — strong local-SEO signal.
     const neighborhoodPages = NEIGHBORHOODS.map(n => ({
       loc: `/neighborhood/${n.slug}`, changefreq: 'daily', priority: '0.7'
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
index d9bf589..5b52894 100644
--- a/views/partials/head.ejs
+++ b/views/partials/head.ejs
@@ -9,10 +9,23 @@
       ? metaDescription
       : 'Ventura Claw — directory and lead-routing service for small businesses on Ventura Blvd.';
     var _canonicalBase = (typeof publicUrl !== 'undefined' && publicUrl) ? publicUrl.replace(/\/+$/, '') : 'https://venturaclaw.com';
-    var _canonicalUrl = _canonicalBase + (typeof path === 'string' ? path : '/');
+    // Canonical priority: explicit canonicalPath (route override) > path (req.path).
+    // Routes with meaningful query params (filters, pagination) should pass
+    // canonicalPath so Google indexes each filter combination separately
+    // instead of folding them all under the bare /find canonical.
+    var _canonicalRel = (typeof canonicalPath === 'string' && canonicalPath)
+      ? canonicalPath
+      : (typeof path === 'string' ? path : '/');
+    var _canonicalUrl = _canonicalBase + _canonicalRel;
   %>
   <meta name="description" content="<%= _metaDesc %>">
   <link rel="canonical" href="<%= _canonicalUrl %>">
+  <% if (typeof prevUrl !== 'undefined' && prevUrl) { %>
+    <link rel="prev" href="<%= prevUrl %>">
+  <% } %>
+  <% if (typeof nextUrl !== 'undefined' && nextUrl) { %>
+    <link rel="next" href="<%= nextUrl %>">
+  <% } %>
   <meta property="og:type" content="website">
   <meta property="og:url" content="<%= _canonicalUrl %>">
   <meta property="og:site_name" content="Ventura Claw">
diff --git a/views/public/find.ejs b/views/public/find.ejs
index 2abfd8e..c5e1a49 100644
--- a/views/public/find.ejs
+++ b/views/public/find.ejs
@@ -27,6 +27,9 @@
 <section class="find-page">
   <p class="kicker">Find a business</p>
   <h1 class="display-sm"><%= customH1 %></h1>
+  <p class="muted" style="margin:-8px 0 16px;font-size:14px">
+    Prefer the map? <a href="/map<%= v ? '?vertical=' + v : '' %>" style="font-weight:600">View these on a map →</a>
+  </p>
 
   <form action="/find" method="get" class="find-filters" role="search" autocomplete="off">
     <div class="filter-row">
diff --git a/views/public/home.ejs b/views/public/home.ejs
index bc1f3f1..a830514 100644
--- a/views/public/home.ejs
+++ b/views/public/home.ejs
@@ -9,6 +9,9 @@
     <input type="search" name="q" placeholder="Try 'salon Sherman Oaks' or 'cafe Studio City'" autocomplete="off">
     <button type="submit" class="btn btn-primary btn-lg">Search →</button>
   </form>
+  <p class="hero-secondary" style="margin:8px 0 0;font-size:14px">
+    or <a href="/map" style="font-weight:600">browse the map →</a> · <%= stats.total %> pins on Ventura Blvd
+  </p>
   <div class="hero-meta">
     <span><strong><%= stats.total %></strong> businesses</span>
     <span><strong><%= stats.vertical_count %></strong> categories</span>

← 3f90e3f Add pagination to /find — page=N param + Prev/Next nav  ·  back to Ventura Claw Leads  ·  Add per-vertical sample rails to home page (food/beauty/reta 05ae6c7 →