← 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
M routes/public.jsM views/public/find.ejs
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 →