[object Object]

← back to Ventura Claw Leads

yolo tick 23: og:image + Twitter Card image + 'Share this listing' button

c73de6a104816ca25c326daf408e021a0251b280 · 2026-05-07 00:26:48 -0700 · Steve Abrams

OPEN GRAPH / TWITTER CARD
- views/partials/head.ejs: optional ogImage param. When passed (and only
  then), emits og:image + og:image:width + og:image:height + twitter:image
  + twitter:card='summary_large_image' + twitter:title + twitter:description.
  When ogImage absent, defaults to twitter:card='summary' (no large image).
- views/public/business.ejs: passes ogImage=publicUrl+photo_path when the
  business has a cover photo. Same path also added to LocalBusiness JSON-LD
  image field for Google.
- Net: a business with a photo now gets a properly attributed
  social-share preview card on iMessage / Slack / Twitter / LinkedIn /
  Facebook / Discord. Without a photo, falls back to the summary card.

SHARE THIS LISTING
- views/public/business.ejs: 'Share' button next to 'Send a message' /
  'Visit website' / 'Call'. Uses navigator.share when available (Mobile
  Safari, Chrome on Android), falls back to navigator.clipboard.writeText
  with a '· copied' status flash.
- Pure progressive enhancement — works without JS for users with
  copy-link muscle memory.

46/46 tests still pass. Verified on prod: og:image renders when photo set,
summary card otherwise; Share button present on every profile.

Files touched

Diff

commit c73de6a104816ca25c326daf408e021a0251b280
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 00:26:48 2026 -0700

    yolo tick 23: og:image + Twitter Card image + 'Share this listing' button
    
    OPEN GRAPH / TWITTER CARD
    - views/partials/head.ejs: optional ogImage param. When passed (and only
      then), emits og:image + og:image:width + og:image:height + twitter:image
      + twitter:card='summary_large_image' + twitter:title + twitter:description.
      When ogImage absent, defaults to twitter:card='summary' (no large image).
    - views/public/business.ejs: passes ogImage=publicUrl+photo_path when the
      business has a cover photo. Same path also added to LocalBusiness JSON-LD
      image field for Google.
    - Net: a business with a photo now gets a properly attributed
      social-share preview card on iMessage / Slack / Twitter / LinkedIn /
      Facebook / Discord. Without a photo, falls back to the summary card.
    
    SHARE THIS LISTING
    - views/public/business.ejs: 'Share' button next to 'Send a message' /
      'Visit website' / 'Call'. Uses navigator.share when available (Mobile
      Safari, Chrome on Android), falls back to navigator.clipboard.writeText
      with a '· copied' status flash.
    - Pure progressive enhancement — works without JS for users with
      copy-link muscle memory.
    
    46/46 tests still pass. Verified on prod: og:image renders when photo set,
    summary card otherwise; Share button present on every profile.
---
 views/partials/head.ejs   | 11 +++++++++++
 views/public/business.ejs | 31 ++++++++++++++++++++++++++++++-
 2 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/views/partials/head.ejs b/views/partials/head.ejs
index 6182e39..d9bf589 100644
--- a/views/partials/head.ejs
+++ b/views/partials/head.ejs
@@ -18,6 +18,17 @@
   <meta property="og:site_name" content="Ventura Claw">
   <meta property="og:title" content="<%= typeof title !== 'undefined' ? title : 'Ventura Claw' %>">
   <meta property="og:description" content="<%= _metaDesc %>">
+  <% if (typeof ogImage !== 'undefined' && ogImage) { %>
+    <meta property="og:image" content="<%= ogImage %>">
+    <meta property="og:image:width" content="1200">
+    <meta property="og:image:height" content="630">
+    <meta name="twitter:card" content="summary_large_image">
+    <meta name="twitter:image" content="<%= ogImage %>">
+    <meta name="twitter:title" content="<%= typeof title !== 'undefined' ? title : 'Ventura Claw' %>">
+    <meta name="twitter:description" content="<%= _metaDesc %>">
+  <% } else { %>
+    <meta name="twitter:card" content="summary">
+  <% } %>
   <meta name="theme-color" content="#0e0e0e">
   <link rel="preconnect" href="https://fonts.googleapis.com">
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
diff --git a/views/public/business.ejs b/views/public/business.ejs
index b41128a..3491151 100644
--- a/views/public/business.ejs
+++ b/views/public/business.ejs
@@ -1,4 +1,9 @@
-<%- include('../partials/head', { title }) %>
+<%- include('../partials/head', {
+  title,
+  ogImage: business.photo_path
+    ? ((typeof publicUrl !== 'undefined' && publicUrl ? publicUrl.replace(/\/+$/, '') : 'https://leads.venturaclaw.com') + business.photo_path)
+    : null
+}) %>
 <%- include('../partials/header') %>
 
 <%
@@ -26,6 +31,7 @@
   if (business.phone) _ldBusiness.telephone = business.phone;
   if (business.email) _ldBusiness.email = business.email;
   if (_site) _ldBusiness.sameAs = [_site];
+  if (business.photo_path) _ldBusiness.image = _publicUrl + business.photo_path;
   if (business.street || business.city) {
     _ldBusiness.address = {
       '@type': 'PostalAddress',
@@ -94,8 +100,31 @@
     <a href="#contact" class="btn btn-primary btn-lg">Send a message</a>
     <% if (_site) { %><a href="<%= _site %>" target="_blank" rel="noopener nofollow" class="btn btn-ghost">Visit website ↗</a><% } %>
     <% if (business.phone) { %><a href="tel:<%= business.phone %>" class="btn btn-ghost">Call <%= business.phone %></a><% } %>
+    <button type="button" class="btn btn-ghost" data-share-url="<%= _publicUrl %>/business/<%= business.slug %>" data-share-title="<%= business.business_name %>">Share <span data-share-status></span></button>
   </div>
 </section>
+<script>
+  (function(){
+    document.querySelectorAll('[data-share-url]').forEach(function(btn){
+      btn.addEventListener('click', async function(){
+        var url = btn.getAttribute('data-share-url');
+        var title = btn.getAttribute('data-share-title');
+        var status = btn.querySelector('[data-share-status]');
+        // Web Share API on mobile/Safari, clipboard fallback elsewhere.
+        if (navigator.share) {
+          try { await navigator.share({ title: title, url: url }); return; }
+          catch(e){ /* user cancelled — fall through to clipboard */ }
+        }
+        try {
+          await navigator.clipboard.writeText(url);
+          if (status) { status.textContent = '· copied'; setTimeout(function(){ status.textContent=''; }, 2000); }
+        } catch(e) {
+          if (status) { status.textContent = '· press ⌘C'; }
+        }
+      });
+    });
+  })();
+</script>
 
 <section class="profile-body">
   <div class="profile-bio">

← 77c0ec3 yolo tick 22: /admin/leads pagination — 50/page with prev/ne  ·  back to Ventura Claw Leads  ·  yolo tick 24: home featured grid — photo-priority within tie e1590f8 →