[object Object]

← back to NationalPaperHangers

og: add og:image + twitter:image to every page (default brand jpg, per-installer override = hero/portfolio/segment image). Adds 8 e2e assertions to business-invariants. Live preview cards no longer render text-only

442cc6c80a4fa9a035f4916231acf73401764b08 · 2026-05-07 08:02:15 -0700 · Steve

Files touched

Diff

commit 442cc6c80a4fa9a035f4916231acf73401764b08
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu May 7 08:02:15 2026 -0700

    og: add og:image + twitter:image to every page (default brand jpg, per-installer override = hero/portfolio/segment image). Adds 8 e2e assertions to business-invariants. Live preview cards no longer render text-only
---
 public/img/og-default.jpg        | Bin 0 -> 110462 bytes
 routes/public.js                 |  10 ++++++++++
 tests/e2e-business-invariants.js |  17 +++++++++++++++--
 views/partials/head.ejs          |   9 +++++++++
 4 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/public/img/og-default.jpg b/public/img/og-default.jpg
new file mode 100644
index 0000000..af8253c
Binary files /dev/null and b/public/img/og-default.jpg differ
diff --git a/routes/public.js b/routes/public.js
index 3e3282d..6a14089 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -204,12 +204,22 @@ router.get('/installer/:slug', async (req, res, next) => {
     var tpl = (chosen && TEMPLATES.indexOf(chosen) !== -1)
       ? 'public/installer-tpl-' + chosen
       : 'public/installer';
+    // OG preview: prefer studio's chosen hero, then their first portfolio shot,
+    // then a segment-matched PD image, then the brand default (head.ejs falls
+    // back to /img/og-default.jpg when ogImage is null).
+    var _settings = installer.template_settings || {};
+    var _segImg = require('../lib/segment-image').pickSegmentImage(installer);
+    var _ogImage = _settings.hero_url
+      || (portfolio[0] && portfolio[0].image_url)
+      || (_segImg && _segImg.file)
+      || null;
     res.render(tpl, {
       title: cityState
         ? `${installer.business_name} — Wallpaper Installer in ${cityState} · National Paper Hangers`
         : `${installer.business_name} · National Paper Hangers`,
       metaDescription: `${installer.business_name}${cityState ? ' is a verified wallcovering installer in ' + cityState : ' — wallcovering installer'}.${matSummary ? ' Specializing in ' + matSummary + '.' : ''} Book a consultation on National Paper Hangers.`,
       canonicalPath: `/installer/${installer.slug}`,
+      ogImage: _ogImage,
       installer, portfolio, reviews, acceptance, credentials, featuredVideos
     });
   } catch (err) { next(err); }
diff --git a/tests/e2e-business-invariants.js b/tests/e2e-business-invariants.js
index 5695767..d8bedde 100644
--- a/tests/e2e-business-invariants.js
+++ b/tests/e2e-business-invariants.js
@@ -86,8 +86,21 @@ function findChromium() {
     const showToggle = await page.$('a:has-text("directory listings")').catch(() => null);
     assert(!!showToggle, 'show/hide directory listings toggle present');
 
-    // ─── Invariant 5 · privacy + terms have correct DW-disclaimer text ────
-    console.log(`\n[invariant 5] privacy + terms include the referral-service disclaimer`);
+    // ─── Invariant 5 · OG image meta on every page ─────────────────────────
+    // Without og:image, link previews on iMessage / Slack / Twitter render
+    // text-only and look broken. Default falls to /img/og-default.jpg.
+    console.log(`\n[invariant 5] og:image meta on every page`);
+    for (const path of ['/', '/find', '/watch', `/installer/${UNCLAIMED_SLUG}`]) {
+      const r = await page.goto(`${BASE}${path}`, { waitUntil: 'domcontentloaded', timeout: 12000 });
+      const ogUrl = await page.$eval('meta[property="og:image"]', el => el.getAttribute('content')).catch(() => null);
+      assert(!!ogUrl && /^https?:\/\//.test(ogUrl), `${path} has og:image with absolute URL`);
+      // Twitter image too
+      const twUrl = await page.$eval('meta[name="twitter:image"]', el => el.getAttribute('content')).catch(() => null);
+      assert(!!twUrl && /^https?:\/\//.test(twUrl), `${path} has twitter:image`);
+    }
+
+    // ─── Invariant 6 · privacy + terms have correct DW-disclaimer text ────
+    console.log(`\n[invariant 6] privacy + terms include the referral-service disclaimer`);
     for (const path of ['/privacy', '/terms']) {
       await page.goto(`${BASE}${path}`, { waitUntil: 'domcontentloaded', timeout: 12000 });
       const html = await page.content();
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
index 382496c..78ebf50 100644
--- a/views/partials/head.ejs
+++ b/views/partials/head.ejs
@@ -21,9 +21,18 @@
   <meta property="og:site_name" content="National Paper Hangers">
   <meta property="og:title" content="<%= typeof title !== 'undefined' ? title : 'National Paper Hangers' %>">
   <meta property="og:description" content="<%= _metaDesc %>">
+  <%
+    // Per-page templates can pass `ogImage` (absolute path or full URL).
+    // Falls back to a brand default (luxury-residential PD archive image)
+    // so iMessage / Slack / Twitter previews never render image-less.
+    var _ogImageRel = (typeof ogImage !== 'undefined' && ogImage) ? ogImage : '/img/og-default.jpg';
+    var _ogImageAbs = /^https?:\/\//.test(_ogImageRel) ? _ogImageRel : (_canonicalBase + _ogImageRel);
+  %>
+  <meta property="og:image" content="<%= _ogImageAbs %>">
   <meta name="twitter:card" content="summary_large_image">
   <meta name="twitter:title" content="<%= typeof title !== 'undefined' ? title : 'National Paper Hangers' %>">
   <meta name="twitter:description" content="<%= _metaDesc %>">
+  <meta name="twitter:image" content="<%= _ogImageAbs %>">
   <meta name="theme-color" content="#0e0e0e">
   <meta name="csrf-token" content="<%= typeof csrfToken !== 'undefined' && csrfToken ? csrfToken : '' %>">
   <link rel="preconnect" href="https://fonts.googleapis.com">

← d145b79 tests: e2e-business-invariants — 27 prod-runnable assertions  ·  back to NationalPaperHangers  ·  tests: e2e-business-invariants — sitemap.xml + robots.txt re 8efbbdb →