← back to Re Flyer Aggregator
property flyers: deep-crawl broker listing pages for real property marketing PDFs (address/OM/for-sale classifier, drops apps/proxies/decks); gallery viewer at /property-flyers, sortable+searchable+density; seeded 9 from homepage crawl, deep crawl running (TK-10708)
930736f83d8d78230d1d9f3fac32e07cbb4c60c4 · 2026-08-19 22:38:44 -0700 · Steve Abrams
Files touched
A public/flyers-found/index.htmlA public/flyers-found/property-flyers.jsonA scripts/property-flyers.mjsM serve-viewers.mjs
Diff
commit 930736f83d8d78230d1d9f3fac32e07cbb4c60c4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 22:38:44 2026 -0700
property flyers: deep-crawl broker listing pages for real property marketing PDFs (address/OM/for-sale classifier, drops apps/proxies/decks); gallery viewer at /property-flyers, sortable+searchable+density; seeded 9 from homepage crawl, deep crawl running (TK-10708)
---
public/flyers-found/index.html | 55 ++++++++++++++++++++++
public/flyers-found/property-flyers.json | 1 +
scripts/property-flyers.mjs | 78 ++++++++++++++++++++++++++++++++
serve-viewers.mjs | 2 +-
4 files changed, 135 insertions(+), 1 deletion(-)
diff --git a/public/flyers-found/index.html b/public/flyers-found/index.html
new file mode 100644
index 0000000..f9506b9
--- /dev/null
+++ b/public/flyers-found/index.html
@@ -0,0 +1,55 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Property Flyers — found on broker sites · Abrams</title>
+<style>
+ :root{--navy:#0E2350;--gold:#E8A81C;--cols:3} *{box-sizing:border-box}
+ body{margin:0;font-family:Inter,-apple-system,Helvetica,Arial,sans-serif;background:#f4f6fb;color:#141414}
+ header{background:var(--navy);color:#fff;padding:18px 26px} header h1{margin:0;font-size:22px} header .s{color:var(--gold);font-size:12px;margin-top:3px}
+ .controls{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e6e9f2;padding:12px 26px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;font-size:13px}
+ .controls input,.controls select{padding:6px 9px;border:1px solid #d6dced;border-radius:6px;font-size:13px}
+ .controls label{color:#6B7280;margin-right:5px}
+ .stat{color:#6B7280;margin-left:auto;font-variant-numeric:tabular-nums}
+ .wrap{padding:20px 26px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px}
+ .fly{background:#fff;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.07);padding:16px 18px;display:flex;flex-direction:column;min-height:130px;text-decoration:none;color:inherit;transition:.12s}
+ .fly:hover{box-shadow:0 6px 20px rgba(14,35,80,.16);transform:translateY(-2px)}
+ .fly .pdf{display:inline-block;background:#E8342710;color:#c0392b;border:1px solid #f0c9c4;border-radius:5px;font-size:10px;font-weight:800;padding:2px 7px;letter-spacing:.5px;align-self:flex-start}
+ .fly .prop{font-family:'Playfair Display',Georgia,serif;font-size:16px;color:var(--navy);margin:10px 0 6px;line-height:1.3}
+ .fly .firm{font-size:12px;color:#6B7280;margin-top:auto} .fly .firm b{color:#0E7C4A;font-weight:600}
+ .fly .open{color:#16357A;font-size:12px;font-weight:600;margin-top:8px}
+ @media(max-width:800px){.wrap{grid-template-columns:1fr}}
+</style></head><body>
+<header><h1>Property Flyers</h1><div class="s">Real broker marketing one-sheets — harvested from CA CRE broker listing pages · Abrams</div></header>
+<div class="controls">
+ <input id="q" placeholder="search property / broker…" size="24">
+ <div><label>Broker</label><select id="fb"><option value="">All</option></select></div>
+ <div><label>Sort</label><select id="sort"><option value="property">Property A→Z</option><option value="firm">Broker A→Z</option><option value="found_at">Newest found</option></select></div>
+ <div><label>Density</label><input type="range" id="dens" min="1" max="5" step="1" value="3"></div>
+ <div class="stat" id="stat"></div>
+</div>
+<div class="wrap" id="grid"></div>
+<script>
+let ALL=[];
+const esc=s=>(s||'').replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c]));
+function card(f){return `<a class="fly" href="${f.pdf}" target="_blank" rel="noopener">
+ <span class="pdf">📄 PDF FLYER</span>
+ <div class="prop">${esc(f.property)}</div>
+ <div class="firm">Listed by <b>${esc(f.firm)}</b><br>${esc(f.domain)}</div>
+ <div class="open">Open flyer ↗</div></a>`;}
+function brokers(){return [...new Set(ALL.map(f=>f.firm))].sort();}
+function render(){const q=document.getElementById('q').value.toLowerCase(),fb=document.getElementById('fb').value,s=document.getElementById('sort').value;
+ let a=ALL.filter(f=>(!fb||f.firm===fb)&&(f.property+' '+f.firm+' '+f.domain).toLowerCase().includes(q));
+ a.sort(s==='found_at'?(x,y)=>(''+(y.found_at||'')).localeCompare(''+(x.found_at||'')):(x,y)=>(''+(x[s]||'')).localeCompare(''+(y[s]||'')));
+ document.getElementById('grid').innerHTML=a.map(card).join('')||'<div style="color:#9aa;padding:20px">No property flyers yet — the crawl may still be running. Reload shortly.</div>';
+ document.getElementById('stat').textContent=`${a.length} property flyers · ${new Set(a.map(f=>f.firm)).size} brokers`;
+ localStorage.pfSort=s;localStorage.pfBroker=fb;}
+function dens(){document.documentElement.style.setProperty('--cols',document.getElementById('dens').value);localStorage.pfDens=document.getElementById('dens').value;}
+['q','fb','sort'].forEach(id=>document.getElementById(id).oninput=document.getElementById(id).onchange=render);
+document.getElementById('dens').oninput=dens;
+function load(){fetch('property-flyers.json?t='+Date.now()).then(r=>r.json()).then(j=>{ALL=j;
+ document.getElementById('fb').innerHTML='<option value="">All</option>'+brokers().map(b=>`<option>${esc(b)}</option>`).join('');
+ if(localStorage.pfBroker)document.getElementById('fb').value=localStorage.pfBroker;
+ if(localStorage.pfSort)document.getElementById('sort').value=localStorage.pfSort;
+ if(localStorage.pfDens)document.getElementById('dens').value=localStorage.pfDens;
+ dens();render();}).catch(()=>document.getElementById('stat').textContent='no data yet — crawl running');}
+load();setInterval(load,15000);
+</script></body></html>
diff --git a/public/flyers-found/property-flyers.json b/public/flyers-found/property-flyers.json
new file mode 100644
index 0000000..bf87ad0
--- /dev/null
+++ b/public/flyers-found/property-flyers.json
@@ -0,0 +1 @@
+[{"firm":"Maven Commercial, Inc.","domain":"mavenproperties.com","property":"Restaurant Brief","pdf":"https://mavenproperties.com/pdf/Restaurant_Brief.pdf","from":"https://mavenproperties.com","found_at":"2026-08-20T00:55:55.954Z"},{"firm":"Maven Commercial, Inc.","domain":"mavenproperties.com","property":"Retail Brief","pdf":"https://mavenproperties.com/pdf/Retail_Brief.pdf","from":"https://mavenproperties.com","found_at":"2026-08-20T00:55:55.954Z"},{"firm":"Silverstone Commercial","domain":"silverstonecommercial.com","property":"2735 Scott Blvd For Sale 326","pdf":"https://silverstonecommercial.com/contents/pdf/2735-Scott-Blvd-For-Sale-326.pdf","from":"https://silverstonecommercial.com","found_at":"2026-08-20T00:58:14.555Z"},{"firm":"Silverstone Commercial","domain":"silverstonecommercial.com","property":"365 Kuck Lane 150426","pdf":"https://silverstonecommercial.com/contents/pdf/365-Kuck-Lane-150426.pdf","from":"https://silverstonecommercial.com","found_at":"2026-08-20T00:58:14.555Z"},{"firm":"Silverstone Commercial","domain":"silverstonecommercial.com","property":"710 Lakeway Dr Sunnyvale 130326","pdf":"https://silverstonecommercial.com/contents/pdf/710-Lakeway-Dr-Sunnyvale-130326.pdf","from":"https://silverstonecommercial.com","found_at":"2026-08-20T00:58:14.555Z"},{"firm":"Prime Commercial Inc","domain":"primecommercialinc.com","property":"40055 Mission Blvd Fremont OM 8.4.26","pdf":"https://primecommercialinc.com/wp-content/uploads/2026/08/40055-Mission-Blvd_Fremont-OM-8.4.26.pdf","from":"https://primecommercialinc.com","found_at":"2026-08-20T01:00:18.331Z"},{"firm":"Prime Commercial Inc","domain":"primecommercialinc.com","property":"1399 Winchester Ste150 San Jose 5.11.26","pdf":"https://primecommercialinc.com/wp-content/uploads/2026/05/1399-Winchester_Ste150-San-Jose-5.11.26.pdf","from":"https://primecommercialinc.com","found_at":"2026-08-20T01:00:18.331Z"},{"firm":"Prime Commercial Inc","domain":"primecommercialinc.com","property":"730 750 Camden Ave SALE 5.12.26","pdf":"https://primecommercialinc.com/wp-content/uploads/2026/03/730-750-Camden-Ave-SALE-5.12.26.pdf","from":"https://primecommercialinc.com","found_at":"2026-08-20T01:00:18.331Z"},{"firm":"Allied Commercial Real Estate Inc","domain":"alliedcommercialrealestate.com","property":"","pdf":"https://alliedcommercialrealestate.com/resources/tools/flyers/","from":"https://alliedcommercialrealestate.com","found_at":"2026-08-20T01:00:28.616Z"}]
\ No newline at end of file
diff --git a/scripts/property-flyers.mjs b/scripts/property-flyers.mjs
new file mode 100644
index 0000000..aa6b662
--- /dev/null
+++ b/scripts/property-flyers.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+// TK-10708 PROPERTY-FLYER FINDER. For each CRE broker, crawl the homepage AND its listing/
+// for-sale/for-lease pages, harvest the PDF marketing one-sheets for SPECIFIC PROPERTIES
+// (address / "For Sale" / Offering Memorandum), and drop the junk (lease apps, proxies,
+// codes of conduct, investor decks). Output = a browsable property-flyer gallery. $0, read-only.
+//
+// Usage: node scripts/property-flyers.mjs [--n 120] [--pages 6] [--delay 900]
+
+import { execFileSync } from 'node:child_process';
+import { writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const args = process.argv.slice(2);
+const arg = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
+const N = parseInt(arg('--n', '120'), 10), PAGES = parseInt(arg('--pages', '6'), 10), DELAY = parseInt(arg('--delay', '800'), 10);
+const OUTDIR = join(ROOT, 'public', 'flyers-found');
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// a PDF is a PROPERTY flyer if it looks like a specific-property one-sheet, and NOT a legal/corp doc.
+const PROP = /(\d{2,5}[-_%\s]+\w+[-_%\s]+(ave|st|blvd|dr|rd|way|street|road|drive|lane|court|place|hwy|highway|pkwy|circle)|offering[-_ ]?mem|\bOM[-_.]|[-_]om[-_.]|for[-_ ]?(sale|lease)|flyer|brochure|marketing[-_ ]?pack|property[-_ ]?(brief|package|flyer)|listing[-_ ]?flyer|deal[-_ ]?sheet|\bpsf\b|sf[-_ ]?available)/i;
+const JUNK = /(application|privacy|terms|\btou\b|conduct|w-?9|credit[-_ ]?app|policy|disclosure|\bnda\b|agreement|vendor|\bguide\b|proxy|supplemental|investor[-_ ]?(deck|present)|annual[-_ ]?(report|transp)|10-?k|whitepaper|newsletter|code[-_ ]?of|tax[-_ ]?info|profile|strategic[-_ ]?deck|commitment|handbook|w9)/i;
+// links worth following to find listings
+const FOLLOW = /(listing|propert|for-?sale|for-?lease|available|inventory|portfolio|investment|space|our-?deals|closed)/i;
+
+const propTitle = (url) => {
+ let f = decodeURIComponent((url.split('/').pop() || '').replace(/\.pdf.*$/i, '').replace(/[%\-_]+/g, ' ').replace(/\s+/g, ' ').trim());
+ return f.replace(/\b\w/g, c => c.toUpperCase()).slice(0, 70) || url;
+};
+const dom = u => { try { return new URL(u.startsWith('http') ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return null; } };
+
+// broker targets — CA commercial firms w/ a website (big-first)
+const rows = execFileSync('psql', ['usre', '-t', '-A', '-F', '\t', '-c',
+ `SELECT coalesce(name,''), website FROM firm WHERE asset_class='commercial' AND hq_state='CA' AND website IS NOT NULL AND website<>'' ORDER BY agent_count DESC NULLS LAST LIMIT ${N * 2}`],
+ { encoding: 'utf8' }).trim().split('\n').filter(Boolean).map(l => l.split('\t'));
+const seenDom = new Set(), targets = [];
+for (const [name, web] of rows) { const d = dom(web); if (d && !seenDom.has(d)) { seenDom.add(d); targets.push({ name, domain: d }); } if (targets.length >= N) break; }
+
+async function get(url) {
+ try { const r = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(12000) }); return r.ok ? await r.text() : ''; } catch { return ''; }
+}
+const abs = (href, base) => { try { return new URL(href, base).href; } catch { return null; } };
+function pdfsIn(html, base) {
+ const out = new Set(), re = /href\s*=\s*["']([^"']+\.pdf[^"']*)["']/gi; let m;
+ while ((m = re.exec(html))) { const u = abs(m[1], base); if (u && PROP.test(u) && !JUNK.test(u)) out.add(u); }
+ return [...out];
+}
+function listingLinks(html, base, host) {
+ const out = new Set(), re = /href\s*=\s*["']([^"']+)["']/gi; let m;
+ while ((m = re.exec(html))) { const u = abs(m[1], base); if (!u) continue; try { if (new URL(u).hostname.replace(/^www\./, '') !== host) continue; } catch { continue; } if (FOLLOW.test(u) && !/\.pdf|\.jpg|\.png/i.test(u)) out.add(u.split('#')[0]); }
+ return [...out].slice(0, PAGES);
+}
+
+async function crawlBroker(t) {
+ const base = 'https://' + t.domain + '/';
+ const home = await get(base); if (!home) return [];
+ const found = new Map(); // url -> flyer
+ const add = (u, pageUrl) => { if (!found.has(u)) found.set(u, { firm: t.name, domain: t.domain, property: propTitle(u), pdf: u, from: pageUrl, found_at: new Date().toISOString() }); };
+ pdfsIn(home, base).forEach(u => add(u, base));
+ const links = listingLinks(home, base, t.domain);
+ for (const lk of links) { const h = await get(lk); if (h) pdfsIn(h, lk).forEach(u => add(u, lk)); await sleep(120); }
+ return [...found.values()];
+}
+
+// pooled crawl
+async function pool(items, conc, fn) { const out = []; let i = 0; const w = async () => { while (i < items.length) { const it = items[i++]; out.push(...await fn(it)); process.stdout.write('.'); await sleep(DELAY); } }; await Promise.all(Array.from({ length: Math.min(conc, items.length) }, w)); return out; }
+
+console.log(`Property-flyer crawl: ${targets.length} CA brokers × up to ${PAGES} listing pages…`);
+const flyers = await pool(targets, 6, crawlBroker);
+// dedup by pdf url
+const seen = new Set(), out = [];
+for (const f of flyers) { if (seen.has(f.pdf)) continue; seen.add(f.pdf); out.push(f); }
+out.sort((a, b) => a.firm.localeCompare(b.firm) || a.property.localeCompare(b.property));
+writeFileSync(join(OUTDIR, 'property-flyers.json'), JSON.stringify(out, null, 0));
+console.log(`\nFound ${out.length} PROPERTY flyers across ${new Set(out.map(f => f.domain)).size} brokers -> public/flyers-found/property-flyers.json`);
+for (const f of out.slice(0, 20)) console.log(` ${f.firm.slice(0, 26).padEnd(26)} ${f.property.slice(0, 46)}`);
diff --git a/serve-viewers.mjs b/serve-viewers.mjs
index ff6b2a1..69cfe98 100644
--- a/serve-viewers.mjs
+++ b/serve-viewers.mjs
@@ -2,7 +2,7 @@ import http from 'node:http'; import { readFileSync, statSync } from 'node:fs';
const PUB = join(dirname(fileURLToPath(import.meta.url)), 'public');
const MIME = {'.html':'text/html','.json':'application/json','.csv':'text/csv','.js':'text/javascript','.css':'text/css','.png':'image/png'};
const ALIAS = { '/':'/sources/index.html', '/sources':'/sources/index.html', '/live.html':'/flyers/live.html',
- '/flyers.json':'/flyers/flyers.json', '/advertising':'/advertising/index.html', '/flyers':'/flyers/index.html' };
+ '/flyers.json':'/flyers/flyers.json', '/advertising':'/advertising/index.html', '/property-flyers':'/flyers-found/index.html', '/flyers-found':'/flyers-found/index.html', '/flyers':'/flyers/index.html' };
http.createServer((q,s)=>{
let u = decodeURIComponent(q.url.split('?')[0]); u = ALIAS[u] || u;
let fp = join(PUB, u);
← dcc7ff8 news: +33 feeds (18 major-broker press scrapes + metro biz-j
·
back to Re Flyer Aggregator
·
sources dashboard: add sortable 'Latest' date column (per-so af78c32 →