← back to Ventura Claw Leads
yolo tick 20: breadcrumb nav + BreadcrumbList JSON-LD on /business/:slug + admin/team last-active sort
2bd5453527e3e7412e8783a46871ad9961b9f3f3 · 2026-05-06 22:48:41 -0700 · Steve Abrams
BREADCRUMB ON PROFILE
- views/public/business.ejs: visible <nav class=breadcrumb> +
matching schema.org BreadcrumbList JSON-LD. 5 levels:
Home › Find a business › <Vertical> › <City> › <Business Name>
Each level (except current) is a clickable URL pointing at the right
filter on /find. The JSON-LD makes the breadcrumb eligible for
Google's rich-result breadcrumb chips above the SERP snippet.
- 2 JSON-LD blocks now per profile (LocalBusiness + BreadcrumbList) —
validates as clean schema.org.
ADMIN/TEAM SORT
- routes/admin.js GET /admin/team: ORDER BY changed from created_at ASC
to a 3-tier sort: self always first → last_login_at DESC NULLS LAST
→ created_at ASC tiebreak. So the team page surfaces actively-using
teammates at the top, dormant ones below, never-signed-in at bottom.
46/46 tests still pass. Verified live: 2 JSON-LD blocks render on prod
with valid Schema.org @types.
Files touched
M routes/admin.jsM views/public/business.ejs
Diff
commit 2bd5453527e3e7412e8783a46871ad9961b9f3f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 22:48:41 2026 -0700
yolo tick 20: breadcrumb nav + BreadcrumbList JSON-LD on /business/:slug + admin/team last-active sort
BREADCRUMB ON PROFILE
- views/public/business.ejs: visible <nav class=breadcrumb> +
matching schema.org BreadcrumbList JSON-LD. 5 levels:
Home › Find a business › <Vertical> › <City> › <Business Name>
Each level (except current) is a clickable URL pointing at the right
filter on /find. The JSON-LD makes the breadcrumb eligible for
Google's rich-result breadcrumb chips above the SERP snippet.
- 2 JSON-LD blocks now per profile (LocalBusiness + BreadcrumbList) —
validates as clean schema.org.
ADMIN/TEAM SORT
- routes/admin.js GET /admin/team: ORDER BY changed from created_at ASC
to a 3-tier sort: self always first → last_login_at DESC NULLS LAST
→ created_at ASC tiebreak. So the team page surfaces actively-using
teammates at the top, dormant ones below, never-signed-in at bottom.
46/46 tests still pass. Verified live: 2 JSON-LD blocks render on prod
with valid Schema.org @types.
---
routes/admin.js | 4 +++-
views/public/business.ejs | 33 +++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/routes/admin.js b/routes/admin.js
index 0890220..7d69edc 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -267,7 +267,9 @@ router.get('/team', async (req, res, next) => {
SELECT id, email, display_name, last_login_at, created_at,
(id = $2) AS is_self
FROM business_users WHERE business_id = $1
- ORDER BY created_at ASC
+ ORDER BY (id = $2) DESC, -- self always first
+ last_login_at DESC NULLS LAST, -- recently-active up top
+ created_at ASC -- tiebreak: oldest member first
`, [bizId, res.locals.currentBusinessUser.id]);
const pendingInvites = await db.many(`
SELECT id, email, issued_at, expires_at
diff --git a/views/public/business.ejs b/views/public/business.ejs
index 6b5d172..66ef3d5 100644
--- a/views/public/business.ejs
+++ b/views/public/business.ejs
@@ -42,6 +42,39 @@
%>
<script type="application/ld+json"><%- JSON.stringify(_ldBusiness) %></script>
+<%
+ // BreadcrumbList — paired with the visible <nav> below. Google uses this
+ // to render rich-result breadcrumb chips above the result snippet.
+ var _crumbs = [
+ { name: 'Home', url: _publicUrl + '/' },
+ { name: 'Find a business', url: _publicUrl + '/find' }
+ ];
+ if (verticalMeta) _crumbs.push({ name: verticalMeta.label, url: _publicUrl + '/find?vertical=' + business.vertical });
+ if (business.city) _crumbs.push({ name: business.city, url: _publicUrl + '/find?city=' + encodeURIComponent(business.city) + (business.vertical ? '&vertical=' + business.vertical : '') });
+ _crumbs.push({ name: business.business_name, url: _publicUrl + '/business/' + business.slug });
+ var _ldCrumbs = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: _crumbs.map(function(c, i){
+ return { '@type': 'ListItem', position: i + 1, name: c.name, item: c.url };
+ })
+ };
+%>
+<script type="application/ld+json"><%- JSON.stringify(_ldCrumbs) %></script>
+
+<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;
+ %>
+ <% if (isLast) { %>
+ <span style="color:var(--fg)"><%= c.name %></span>
+ <% } else { %>
+ <a href="<%= c.url.replace(_publicUrl, '') %>" style="border:none;color:var(--fg-muted)"><%= c.name %></a>
+ <span style="margin:0 6px;opacity:0.5">›</span>
+ <% } %>
+ <% }); %>
+</nav>
+
<% if (business.photo_path) { %>
<div class="vertical-hero vertical-hero-photo">
<img src="<%= business.photo_path %>" alt="<%= business.business_name %>" loading="lazy" decoding="async">
← 863a780 yolo tick 19: sharp resize + WebP convert + EXIF strip on ph
·
back to Ventura Claw Leads
·
yolo tick 21: per-business profile-view counter (with bot+se eb723c7 →