← back to Marketing Command Center
vendors: add LinkedIn vendor→company mapping + public OG harvester (reference/attribution-amplify, no API/auth)
6ccae80ad5f3fc73c87b2f5f1c30a7995f06e97e · 2026-08-31 12:18:48 -0700 · Steve Abrams
Files touched
A data/vendor-linkedin.jsonM modules/vendors/index.js
Diff
commit 6ccae80ad5f3fc73c87b2f5f1c30a7995f06e97e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 12:18:48 2026 -0700
vendors: add LinkedIn vendor→company mapping + public OG harvester (reference/attribution-amplify, no API/auth)
---
data/vendor-linkedin.json | 44 ++++++++++++++++++++++
modules/vendors/index.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 140 insertions(+)
diff --git a/data/vendor-linkedin.json b/data/vendor-linkedin.json
new file mode 100644
index 0000000..ae2acb2
--- /dev/null
+++ b/data/vendor-linkedin.json
@@ -0,0 +1,44 @@
+{
+ "note": "Vendor → LinkedIn COMPANY page mapping for the reference/attribution-amplify workflow. slug = the linkedin.com/company/<slug> path. verified=true means the slug was EMPIRICALLY confirmed (public Open-Graph GET returned 200 with an og:title/description matching the correct DW-vendor brand); verified=false is a best-guess candidate a human must confirm before it's trusted (LinkedIn rate-limits/999-walls some OG probes, so 'unconfirmed' does not always mean 'wrong'). companyUrl is derived from slug. Harvest is a public Open-Graph pull of the PUBLIC company page only — NO LinkedIn API, NO auth, NO login-scraping. Reference + attribution-amplify ONLY: browse, copy caption text, share WITH attribution back to the vendor. Never download their image into DW's own asset library or repost their image as DW's own.",
+ "verifiedAt": "2026-08-31",
+ "accounts": [
+ { "vendorCode": "dw", "brand": "Designer Wallcoverings", "slug": "designerwallcoverings", "verified": true, "note": "our own company page (OG confirmed)" },
+ { "vendorCode": "1838", "brand": "1838 Wallcoverings", "slug": "1838-wallcoverings", "verified": true },
+ { "vendorCode": "arteriors", "brand": "Arteriors Home", "slug": "arteriors-home", "verified": true, "note": "ARTERIORS Home luxury lighting/furniture — NOT the Boston faux-painting 'Arteriors'" },
+ { "vendorCode": "atomic50", "brand": "Atomic50 Ceilings", "slug": null, "verified": false, "note": "no LinkedIn company page found" },
+ { "vendorCode": "bespoke", "brand": "Bespoke Wallcoverings", "slug": null, "verified": false, "note": "no LinkedIn company page found" },
+ { "vendorCode": "bnwalls", "brand": "BN Walls", "slug": "bnwallcoverings", "verified": true, "note": "Bnwallcoverings (BN Walls / Vescom group) — OG confirmed 200" },
+ { "vendorCode": "brewster", "brand": "Brewster Home Fashions", "slug": "brewster-home-fashions", "verified": true },
+ { "vendorCode": "colouranddesign", "brand": "Colour & Design", "slug": "colour-&-design", "verified": true, "note": "Koroseal family — OG confirmed (slug URL-encodes the &)" },
+ { "vendorCode": "contrado", "brand": "Contrado", "slug": "contrado-imaging-ltd", "verified": false, "note": "candidate 404'd on probe — needs a human to find the live slug" },
+ { "vendorCode": "dedar", "brand": "Dedar", "slug": "dedar", "verified": true, "note": "DEDAR MILANO — OG confirmed" },
+ { "vendorCode": "denovowall", "brand": "DeNovo Wall", "slug": null, "verified": false, "note": "no LinkedIn company page found (LBIBoyd rep line)" },
+ { "vendorCode": "designtex", "brand": "Designtex", "slug": "designtex", "verified": true },
+ { "vendorCode": "fabricut", "brand": "Fabricut", "slug": "fabricut", "verified": true },
+ { "vendorCode": "folia", "brand": "Folia Fabrics", "slug": null, "verified": false, "note": "no LinkedIn company page found (source retired)" },
+ { "vendorCode": "hygge", "brand": "Hygge & West", "slug": "hygge-&-west", "verified": false, "note": "candidate 404'd on probe — needs a human to find the live slug" },
+ { "vendorCode": "justindavid", "brand": "Justin David Textiles", "slug": null, "verified": false, "note": "private-label upstream — do NOT surface customer-facing; confirm before use" },
+ { "vendorCode": "knoll", "brand": "Knoll Textiles", "slug": "knoll", "verified": true, "note": "Knoll (MillerKnoll) parent; Knoll Textiles is a division" },
+ { "vendorCode": "maya-romanoff", "brand": "Maya Romanoff", "slug": "maya-romanoff", "verified": true },
+ { "vendorCode": "mindthegap", "brand": "MINDTHEGAP", "slug": "mindtheg", "verified": true, "note": "MINDTHEGAP International — OG confirmed" },
+ { "vendorCode": "muralsource", "brand": "Mural Source", "slug": null, "verified": false, "note": "no LinkedIn company page found" },
+ { "vendorCode": "naturale54", "brand": "Naturale 54", "slug": null, "verified": false, "note": "no LinkedIn company page found (LBIBoyd rep line)" },
+ { "vendorCode": "osborne", "brand": "Osborne & Little", "slug": "osborne-&-little", "verified": false, "note": "candidate 404'd on probe — needs a human to find the live slug" },
+ { "vendorCode": "rebel-walls", "brand": "Rebel Walls", "slug": "rebel-walls", "verified": true, "note": "OG resolves to 'Gimmersta Wallpaper' — Rebel Walls' parent (Gimmersta Wallpaper AB); correct entity" },
+ { "vendorCode": "romo", "brand": "Romo", "slug": "romo-group", "verified": true, "note": "ROMO GROUP parent" },
+ { "vendorCode": "stout", "brand": "Stout Textiles", "slug": "stout-inc", "verified": true, "note": "Stout Inc — OG confirmed" },
+ { "vendorCode": "timorous", "brand": "Timorous Beasties", "slug": "timorousbeasties", "verified": true, "note": "OG confirmed (no hyphens in the slug)" },
+ { "vendorCode": "wallquest", "brand": "WallQuest", "slug": null, "verified": false, "note": "private-label upstream (Malibu) — no confirmed slug; do NOT surface customer-facing" },
+ { "vendorCode": "york", "brand": "York Wallcoverings", "slug": "york-wallcoverings", "verified": true },
+ { "vendorCode": "schumacher", "brand": "F. Schumacher", "slug": "f-schumacher-&-co", "verified": false, "note": "brand policy: INTERNAL-ONLY, must not be amplified customer-facing; slug unconfirmed" },
+ { "vendorCode": "thibaut", "brand": "Thibaut", "slug": "thibaut-inc", "verified": true, "note": "Thibaut Inc — OG confirmed" },
+ { "vendorCode": "arte", "brand": "Arte International", "slug": "arte", "verified": true, "note": "ARTE (Belgium) — OG confirmed" },
+ { "vendorCode": "koroseal", "brand": "Koroseal", "slug": "koroseal", "verified": true, "note": "Koroseal Interior Products — OG confirmed" },
+ { "vendorCode": "wolf-gordon", "brand": "Wolf Gordon", "slug": "wolf-gordon", "verified": true, "note": "Wolf-Gordon — OG confirmed" },
+ { "vendorCode": "carnegie", "brand": "Carnegie Fabrics", "slug": "carnegie", "verified": true, "note": "Carnegie — OG confirmed" },
+ { "vendorCode": "scalamandre", "brand": "Scalamandré", "slug": "scalamandre", "verified": true, "note": "Scalamandré Luxury Fabrics & Wallcoverings — OG confirmed" },
+ { "vendorCode": "sister-parish", "brand": "Sister Parish Design", "slug": "sisterparishdesign", "verified": true, "note": "Sister Parish Design — OG confirmed (no hyphens)" },
+ { "vendorCode": "cole-son", "brand": "Cole & Son", "slug": "cole-&-son", "verified": true, "note": "Cole & Son — OG confirmed (slug URL-encodes the &)" },
+ { "vendorCode": "phillip-jeffries", "brand": "Phillip Jeffries", "slug": "phillip-jeffries-ltd", "verified": false, "note": "candidate 404'd on probe — needs a human to find the live slug" }
+ ]
+}
diff --git a/modules/vendors/index.js b/modules/vendors/index.js
index 89848c7..7a586da 100644
--- a/modules/vendors/index.js
+++ b/modules/vendors/index.js
@@ -10,10 +10,19 @@ const path = require('path');
const GRAPH = 'https://graph.facebook.com/v23.0';
const DATA = path.join(__dirname, '..', '..', 'data', 'vendor-instagram.json');
const CACHE = path.join(__dirname, '..', '..', 'data', 'vendor-posts-cache.json');
+// LinkedIn vendor→company mapping + public-OG post cache (reference/attribution-
+// amplify only — NO LinkedIn API, NO auth, NO scraping-around-login; a read-only
+// Open-Graph GET of the PUBLIC company/post page, exactly like the linkedin
+// module's fetchOg helper).
+const LI_DATA = path.join(__dirname, '..', '..', 'data', 'vendor-linkedin.json');
+const LI_CACHE = path.join(__dirname, '..', '..', 'data', 'vendor-linkedin-cache.json');
function load() { try { return JSON.parse(fs.readFileSync(DATA, 'utf8')); } catch { return []; } }
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE, 'utf8')); } catch { return {}; } }
function saveCache(c) { fs.writeFileSync(CACHE, JSON.stringify(c, null, 2)); }
+function loadLi() { try { return JSON.parse(fs.readFileSync(LI_DATA, 'utf8')); } catch { return { accounts: [] }; } }
+function loadLiCache() { try { return JSON.parse(fs.readFileSync(LI_CACHE, 'utf8')); } catch { return {}; } }
+function saveLiCache(c) { try { fs.mkdirSync(path.dirname(LI_CACHE), { recursive: true }); fs.writeFileSync(LI_CACHE, JSON.stringify(c, null, 2)); } catch { /* non-fatal */ } }
function envFrom(file, k) { try { return (fs.readFileSync(file, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')) || [])[1]; } catch { return null; } }
// Resolve a usable Meta token — prefer a never-expiring PAGE token. Checks
// META_ACCESS_TOKEN then IG_ACCESS_TOKEN, in MCC .env then the secrets master.
@@ -74,6 +83,39 @@ async function fetchPosts(igId, token, username) {
};
}
+// ── LinkedIn public Open-Graph harvest (reference/attribution-amplify) ────────
+// LinkedIn has NO public feed-read API and its User Agreement bans scraping-
+// around-login, so this is a best-effort READ-ONLY Open-Graph GET of the PUBLIC
+// company page (linkedin.com/company/<slug>) — same technique as the linkedin
+// module's fetchOg. It returns the company page's og:title/og:image/og:description
+// (a thumbnail + text + the permalink). It NEVER logs in, never hits an API,
+// never downloads the image into DW's assets — the amplify UX copies the text and
+// shares a link WITH attribution back to the vendor.
+const LI_UA = 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)';
+async function fetchLiOg(url) {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), 8000);
+ try {
+ const r = await fetch(url, { headers: { 'User-Agent': LI_UA, 'Accept-Language': 'en-US,en;q=0.9' }, redirect: 'follow', signal: ctrl.signal });
+ const status = r.status;
+ if (!r.ok) return { ok: false, status, error: 'HTTP ' + status + (status === 999 || status === 429 ? ' (LinkedIn rate-limited / auth-walled this fetch)' : '') };
+ const html = (await r.text()).slice(0, 400000);
+ const og = k => { const m = html.match(new RegExp('<meta[^>]+(?:property|name)=["\\\']og:' + k + '["\\\'][^>]+content=["\\\']([^"\\\']+)', 'i')); return m ? m[1].replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"') : ''; };
+ return { ok: true, status, title: og('title'), thumb: og('image'), description: og('description') };
+ } catch (e) {
+ return { ok: false, error: /abort/i.test(e.message) ? 'timeout' : e.message };
+ } finally { clearTimeout(timer); }
+}
+// Reference/attribution-amplify kit for a LinkedIn company: copyable text that
+// CREDITS the vendor + links back to their public page, plus a DW-visit CTA. No
+// image is ever pulled into DW assets — attribution + link-share only.
+function liAmplifyKit(brand, companyUrl, og) {
+ const DWUTM = 'https://designerwallcoverings.com/?utm_source=linkedin&utm_medium=social&utm_campaign=vendor-amplify';
+ const snippet = String(og && og.description || og && og.title || '').replace(/\s+/g, ' ').trim().slice(0, 220);
+ const text = `${brand}${snippet ? ' — ' + snippet : ''}\n\n🔗 via ${brand} on LinkedIn: ${companyUrl}\n🛍️ Explore the line at Designer Wallcoverings: ${DWUTM}\n\n#DesignerWallcoverings #wallcoverings #interiordesign`;
+ return { text, dwLink: DWUTM };
+}
+
module.exports = {
id: 'vendors',
title: 'Vendor IG Reporting',
@@ -181,5 +223,59 @@ module.exports = {
res.json({ ok: false, handle, error: e.message + (/rate|limit|#4|#17/i.test(e.message) ? ' — rate-limited, try again shortly' : '') });
}
});
+
+ // ── LinkedIn (reference/attribution-amplify) ─────────────────────────────
+ // GET the vendor→LinkedIn-company roster merged with the cached OG pull +
+ // a ready-to-copy attribution-amplify kit per vendor. READ-ONLY; no API.
+ router.get('/linkedin/accounts', (_req, res) => {
+ const cfg = loadLi();
+ const cache = loadLiCache();
+ const accounts = (cfg.accounts || []).map(a => {
+ const companyUrl = a.slug ? `https://www.linkedin.com/company/${a.slug}/` : null;
+ const c = a.slug ? (cache[a.slug] || null) : null;
+ const og = c && c.og ? c.og : null;
+ return {
+ vendorCode: a.vendorCode, brand: a.brand, slug: a.slug || null,
+ verified: a.verified === true, note: a.note || '',
+ hasLinkedIn: !!a.slug, companyUrl,
+ og, fetchedAt: c ? c.fetchedAt : null, error: c ? c.error : null,
+ amplify: (companyUrl && og) ? liAmplifyKit(a.brand, companyUrl, og) : null,
+ };
+ });
+ const withLi = accounts.filter(a => a.hasLinkedIn);
+ res.json({
+ accounts,
+ stats: {
+ total: accounts.length,
+ withLinkedIn: withLi.length,
+ verified: withLi.filter(a => a.verified).length,
+ unverified: withLi.filter(a => !a.verified).length,
+ missing: accounts.length - withLi.length,
+ harvested: accounts.filter(a => a.og).length,
+ harvestedAt: Object.values(cache).map(c => c.fetchedAt).filter(Boolean).sort().pop() || null,
+ },
+ });
+ });
+
+ // POST /linkedin/harvest — pull the PUBLIC OG (thumbnail + text) for every
+ // mapped vendor company page (or ?vendorCode= for one). Read-only GETs; no
+ // login, no API. Rate-gentle. Honest per-vendor error surfacing (LinkedIn may
+ // auth-wall / rate-limit a given fetch — the embed/link still works).
+ router.post('/linkedin/harvest', async (req, res) => {
+ const only = String((req.body && req.body.vendorCode) || (req.query && req.query.vendorCode) || '').trim();
+ const cfg = loadLi();
+ const cache = loadLiCache();
+ const targets = (cfg.accounts || []).filter(a => a.slug && (!only || a.vendorCode === only));
+ let ok = 0, failed = 0;
+ for (const a of targets) {
+ const companyUrl = `https://www.linkedin.com/company/${a.slug}/`;
+ const og = await fetchLiOg(companyUrl);
+ if (og.ok) { cache[a.slug] = { og: { title: og.title, thumb: og.thumb, description: og.description }, status: og.status, fetchedAt: new Date().toISOString(), error: null }; ok++; }
+ else { cache[a.slug] = { ...(cache[a.slug] || {}), status: og.status || null, fetchedAt: new Date().toISOString(), error: og.error || 'fetch failed' }; failed++; }
+ saveLiCache(cache);
+ await sleep(800); // be gentle — LinkedIn rate-limits aggressive OG pulls
+ }
+ res.json({ ok: true, harvested: ok, failed, total: targets.length });
+ });
},
};
← d56a62c TK-10519: safe no-publish TikTok integration demo video (rec
·
back to Marketing Command Center
·
vendors panel: add 'Pull ALL vendor accounts' + IG coverage b8fe12e →