[object Object]

← back to Ventura Claw Leads

Add pagination to /find — page=N param + Prev/Next nav

3f90e3fc40e98406d4a8801b7576b0a76fc983d2 · 2026-05-07 10:02:31 -0700 · Steve Abrams

Beauty has 636 rows but the LIMIT 200 made everything past row 200
unreachable. Add ?page=N (200/page, capped at 50) with stable id
tiebreaker on ORDER BY so a row never appears on two pages. Pagination
nav at the bottom of the grid uses rel='prev'/rel='next' for SEO.

Top of file says 'Showing 200 of 636 businesses' on capped pages, plain
'56 businesses found' when the page contains everything. Beauty=4
pages, food=3, retail=2, creative=2; the others fit on one page.

Files touched

Diff

commit 3f90e3fc40e98406d4a8801b7576b0a76fc983d2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 10:02:31 2026 -0700

    Add pagination to /find — page=N param + Prev/Next nav
    
    Beauty has 636 rows but the LIMIT 200 made everything past row 200
    unreachable. Add ?page=N (200/page, capped at 50) with stable id
    tiebreaker on ORDER BY so a row never appears on two pages. Pagination
    nav at the bottom of the grid uses rel='prev'/rel='next' for SEO.
    
    Top of file says 'Showing 200 of 636 businesses' on capped pages, plain
    '56 businesses found' when the page contains everything. Beauty=4
    pages, food=3, retail=2, creative=2; the others fit on one page.
---
 routes/public.js      | 20 ++++++++++++++++----
 views/public/find.ejs | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/routes/public.js b/routes/public.js
index 59c4d94..9e6eea5 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -92,13 +92,23 @@ router.get('/find', async (req, res, next) => {
     };
     const orderBy = orderClauses[sortKey] || orderClauses.featured;
 
+    // Page param: 1-indexed, capped at a reasonable max so Google can crawl
+    // them all but a malicious actor can't ask for page=99999. Each page is
+    // 200 rows. Adding ORDER BY id as a tiebreaker keeps the order stable
+    // across pages so a row never appears on two pages.
+    const PAGE_SIZE = 200;
+    const MAX_PAGE = 50;
+    const requestedPage = parseInt(req.query.page || '1', 10);
+    const page = Math.max(1, Math.min(MAX_PAGE, isNaN(requestedPage) ? 1 : requestedPage));
+    const offset = (page - 1) * PAGE_SIZE;
+
     const businesses = await db.many(`
       SELECT id, slug, business_name, vertical, headline, neighborhood, city, state, tier, claim_status, verified, photo_path
         FROM businesses
        WHERE ${where.join(' AND ')}
-       ORDER BY ${orderBy}
-       LIMIT 200
-    `, params);
+       ORDER BY ${orderBy}, id ASC
+       LIMIT ${PAGE_SIZE} OFFSET $${params.length + 1}
+    `, [...params, offset]);
 
     // Per-vertical custom title + H1 = real SEO value. Each vertical filter is
     // a distinct landing page in Google's eyes — 8 vertical-specific URLs that
@@ -134,12 +144,14 @@ router.get('/find', async (req, res, next) => {
       ? `${titlePartsList.join(' ')} on Ventura Blvd`
       : 'Search Ventura Blvd';
 
+    const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
     res.render('public/find', {
       title: customTitle,
       metaDescription: customMeta,
       businesses, totalCount, q, v, city, verticals: VERTICALS,
       sort: orderClauses[sortKey] ? sortKey : 'featured',
-      customH1, verticalLabel
+      customH1, verticalLabel,
+      page, totalPages, pageSize: PAGE_SIZE
     });
   } catch (err) { next(err); }
 });
diff --git a/views/public/find.ejs b/views/public/find.ejs
index 62d353b..2abfd8e 100644
--- a/views/public/find.ejs
+++ b/views/public/find.ejs
@@ -117,6 +117,26 @@
         </a>
       <% }); %>
     </div>
+
+    <% if (typeof totalPages !== 'undefined' && totalPages > 1) {
+         // Build a query string preserving current filters but swapping page=
+         var qs = '';
+         if (q) qs += '&q=' + encodeURIComponent(q);
+         if (v) qs += '&vertical=' + encodeURIComponent(v);
+         if (city) qs += '&city=' + encodeURIComponent(city);
+         if (sort && sort !== 'featured') qs += '&sort=' + encodeURIComponent(sort);
+         function pageUrl(n){ return '/find?page=' + n + qs; }
+    %>
+      <nav class="pagination" role="navigation" aria-label="Pagination" style="display:flex;gap:8px;justify-content:center;align-items:center;margin:32px 0 16px;flex-wrap:wrap">
+        <% if (page > 1) { %>
+          <a href="<%= pageUrl(page - 1) %>" class="btn" rel="prev">← Prev</a>
+        <% } %>
+        <span class="muted" style="font-size:13px">Page <%= page %> of <%= totalPages %></span>
+        <% if (page < totalPages) { %>
+          <a href="<%= pageUrl(page + 1) %>" class="btn btn-primary" rel="next">Next →</a>
+        <% } %>
+      </nav>
+    <% } %>
   <% } %>
 </section>
 

← c64097d Per-vertical SEO meta uses real total + city names; visible  ·  back to Ventura Claw Leads  ·  Sitemap covers paginated pages; head supports rel=prev/next 5cc49d8 →