← 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
A public/img/og-default.jpgM routes/public.jsM tests/e2e-business-invariants.jsM views/partials/head.ejs
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 →