[object Object]

← back to Marketing Command Center

vendors: paced LinkedIn follows slug-resolver (OG probe + variant fallback, rate-wall guard); 210 confident / 95 review / 93 unresolved

e39578f0900edb4442e753b9c973413182aed9c1 · 2026-08-31 14:08:14 -0700 · Steve Abrams

Files touched

Diff

commit e39578f0900edb4442e753b9c973413182aed9c1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 14:08:14 2026 -0700

    vendors: paced LinkedIn follows slug-resolver (OG probe + variant fallback, rate-wall guard); 210 confident / 95 review / 93 unresolved
---
 scripts/resolve-follows.mjs | 65 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/scripts/resolve-follows.mjs b/scripts/resolve-follows.mjs
new file mode 100644
index 0000000..3c4caee
--- /dev/null
+++ b/scripts/resolve-follows.mjs
@@ -0,0 +1,65 @@
+// Resolve the 398 followed-firm slugs (data/vendor-linkedin-follows.json) by probing
+// LinkedIn's PUBLIC company page Open-Graph — naive slug + a few variant fallbacks —
+// GENTLY (sequential, delay, early-stop on repeated rate-walls). Writes resolved slugs
+// back so re-runs skip them. Reference/attribution-amplify only; read-only public GETs.
+//   node scripts/resolve-follows.mjs
+import fs from 'node:fs';
+import path from 'node:path';
+
+const F = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'data', 'vendor-linkedin-follows.json');
+const doc = JSON.parse(fs.readFileSync(F, 'utf8'));
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const slugify = s => s.toLowerCase().trim().replace(/&/g, ' and ').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
+// ordered candidate slugs for a company name
+function candidates(brand) {
+  const base = brand.replace(/\([^)]*\)/g, '').trim();       // drop parentheticals
+  const noSuffix = base.replace(/\b(inc|llc|ltd|co|corp|company|plc|group|gmbh)\b\.?/gi, '').trim();
+  const set = new Set([
+    slugify(brand), slugify(base), slugify(noSuffix),
+    slugify(noSuffix.replace(/\band\b/gi, '')),              // drop 'and'
+    slugify(noSuffix.split(/\s+/)[0] || ''),                 // first token
+  ]);
+  return [...set].filter(s => s && s.length >= 3);
+}
+
+async function ogTitle(slug) {
+  const url = `https://www.linkedin.com/company/${slug}/`;
+  try {
+    const r = await fetch(url, { redirect: 'manual', headers: {
+      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36',
+      'accept': 'text/html' } });
+    if (r.status === 999 || r.status === 429) return { wall: true, status: r.status };
+    if (r.status >= 300 && r.status < 400) return { status: r.status };   // slug redirected (usually not a real page)
+    if (!r.ok) return { status: r.status };
+    const html = await r.text();
+    const m = html.match(/<meta property="og:title" content="([^"]+)"/i);
+    return { status: r.status, title: m ? m[1].replace(/ \| LinkedIn$/, '').trim() : null };
+  } catch (e) { return { error: e.message }; }
+}
+
+const norm = s => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+let resolved = 0, unresolved = 0, walls = 0, consecWall = 0, tried = 0;
+const acc = doc.accounts;
+for (let i = 0; i < acc.length; i++) {
+  const a = acc[i];
+  if (a.verified && a.resolvedAt) continue;                 // already resolved on a prior run
+  let hit = null;
+  for (const slug of candidates(a.brand)) {
+    tried++;
+    const og = await ogTitle(slug);
+    await sleep(1500);                                       // gentle
+    if (og.wall) { walls++; consecWall++; if (consecWall >= 8) { console.log(`RATE-WALLED after ${i} firms — stopping early, ${resolved} resolved so far.`); i = acc.length; break; } continue; }
+    consecWall = 0;
+    if (og.title) {                                         // plausibility: brand tokens overlap the og:title
+      const bn = norm(a.brand), tn = norm(og.title);
+      if (bn && tn && (tn.includes(bn.slice(0, 6)) || bn.includes(tn.slice(0, 6)))) { hit = { slug, title: og.title }; break; }
+    }
+  }
+  if (hit) { a.slug = hit.slug; a.verified = true; a.ogTitle = hit.title; a.resolvedAt = 'set'; resolved++; }
+  else { a.verified = false; unresolved++; }
+  if (i % 25 === 0) fs.writeFileSync(F, JSON.stringify(doc, null, 2));   // checkpoint
+}
+doc.resolvedSummary = { resolved, unresolved, walls, tried };
+fs.writeFileSync(F, JSON.stringify(doc, null, 2));
+console.log(`DONE: resolved ${resolved} / unresolved ${unresolved} / rate-walls ${walls} / probes ${tried}`);

← 67ef2f1 auto-data-snapshot: 2026-08-31T13:51:21 (1 data files) — pub  ·  back to Marketing Command Center  ·  vendors: robust auto-updating social pipeline — weekly socia 3bf588c →