[object Object]

← 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

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 →