← back to Rentv
PR intel: add national CRE media + association-media + PR agencies
875aaec0fdb59f66aef10275d9ffeb5efacfcf78 · 2026-08-05 13:00:24 -0700 · Steve
- migration 004: submission_page_url / editorial_calendar_url / awards_url /
events_url / media_kit_url columns on pr_organizations
- national-media-seed.js (npm run pr:seed:national): 54 curated candidates —
national CRE publications, vertical trade media, regional business journals
(ACBJ + independents), industry-association media, national CRE PR agencies
- taxonomy: editorial + media-business role rules (editor/reporter/publisher/
assignment+newsletter editor/sponsored-content/ad-partnerships) -> editorial
& media_business departments, weighted for outreach priority
- website adapter: auto-detect submission/editorial-calendar/awards/events/
media-kit pages during verify-organization; thread new fields through
create()/update() and the verify handler
- honest-by-construction: orgs seeded as 'discovered'; verify pipeline fills
press pages, emails, editorial contacts from live sites (no fabricated data)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M package.jsonM src/pr/adapters/website.jsM src/pr/jobs/index.jsM src/pr/lib/taxonomy.jsA src/pr/migrations/004_media_pr_fields.sqlA src/pr/seed/national-media-seed.jsM src/pr/services/organizations.js
Diff
commit 875aaec0fdb59f66aef10275d9ffeb5efacfcf78
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 5 13:00:24 2026 -0700
PR intel: add national CRE media + association-media + PR agencies
- migration 004: submission_page_url / editorial_calendar_url / awards_url /
events_url / media_kit_url columns on pr_organizations
- national-media-seed.js (npm run pr:seed:national): 54 curated candidates —
national CRE publications, vertical trade media, regional business journals
(ACBJ + independents), industry-association media, national CRE PR agencies
- taxonomy: editorial + media-business role rules (editor/reporter/publisher/
assignment+newsletter editor/sponsored-content/ad-partnerships) -> editorial
& media_business departments, weighted for outreach priority
- website adapter: auto-detect submission/editorial-calendar/awards/events/
media-kit pages during verify-organization; thread new fields through
create()/update() and the verify handler
- honest-by-construction: orgs seeded as 'discovered'; verify pipeline fills
press pages, emails, editorial contacts from live sites (no fabricated data)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
package.json | 3 +-
src/pr/adapters/website.js | 15 ++-
src/pr/jobs/index.js | 6 +
src/pr/lib/taxonomy.js | 26 +++-
src/pr/migrations/004_media_pr_fields.sql | 16 +++
src/pr/seed/national-media-seed.js | 196 ++++++++++++++++++++++++++++++
src/pr/services/organizations.js | 13 +-
7 files changed, 268 insertions(+), 7 deletions(-)
diff --git a/package.json b/package.json
index fb1bc3f1..fc7361d3 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"pr:worker": "node src/pr/worker.js",
"pr:seed:ca": "node src/pr/seed/ca-seed.js",
"test": "node --test test/pr/*.test.js",
- "pr:seed:az": "node src/pr/seed/az-seed.js"
+ "pr:seed:az": "node src/pr/seed/az-seed.js",
+ "pr:seed:national": "node src/pr/seed/national-media-seed.js"
},
"dependencies": {
"express": "^4.21.2",
diff --git a/src/pr/adapters/website.js b/src/pr/adapters/website.js
index 8c860ddb..cf5db05e 100644
--- a/src/pr/adapters/website.js
+++ b/src/pr/adapters/website.js
@@ -9,6 +9,12 @@ const PAGE_HINTS = {
press: /news(room)?|press|media(-|\s)?(center|centre|room|contact|kit)|announcements/i,
team: /team|people|leadership|our-people|professionals|staff|about-us\/(team|people|leadership)|management/i,
contact: /contact/i,
+ // Media/PR outreach pages (publications, trade media, association media):
+ submission: /submit|press[\s-]?release|news[\s-]?tip|share[\s-]?(your[\s-]?)?news|send[\s-]?(us[\s-]?)?news|contribute|editorial[\s-]?submission|for[\s-]?(the[\s-]?)?press|story[\s-]?ideas?/i,
+ editorial_calendar: /editorial[\s-]?calendar|media[\s-]?(planner|guide|pack)|content[\s-]?calendar/i,
+ awards: /awards?|best[\s-]?of|honou?rs|rankings?|power[\s-]?(list|100|players)/i,
+ events: /events?|conferences?|webinars?|summit|forum/i,
+ media_kit: /media[\s-]?kit|advertise|advertising|sponsorship|partnerships?/i,
};
const EMAIL_RE = /\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b/gi;
@@ -200,7 +206,7 @@ const adapter = register({
*/
async profileOrganization(websiteUrl) {
const errors = [];
- const item = { website_ok: false, press_page_url: null, newsroom_url: null, contact_form_url: null, general_press_email: null, general_press_phone: null, press_emails: [], team_pages: [], people: [], evidence: [] };
+ const item = { website_ok: false, press_page_url: null, newsroom_url: null, contact_form_url: null, general_press_email: null, general_press_phone: null, press_emails: [], team_pages: [], people: [], evidence: [], submission_page_url: null, editorial_calendar_url: null, awards_url: null, events_url: null, media_kit_url: null };
let home;
try {
home = await politeFetch(websiteUrl, { provider: 'website' });
@@ -223,6 +229,13 @@ const adapter = register({
item.contact_form_url = firstMatch(PAGE_HINTS.contact);
const teamUrl = firstMatch(PAGE_HINTS.team);
if (teamUrl) item.team_pages.push(teamUrl);
+ // Media/PR outreach pages — link-level discovery from the homepage nav/footer.
+ // editorial_calendar checked before events so "editorial calendar" ≠ a plain event calendar.
+ item.submission_page_url = firstMatch(PAGE_HINTS.submission);
+ item.editorial_calendar_url = firstMatch(PAGE_HINTS.editorial_calendar);
+ item.awards_url = firstMatch(PAGE_HINTS.awards);
+ item.events_url = firstMatch(PAGE_HINTS.events);
+ item.media_kit_url = firstMatch(PAGE_HINTS.media_kit);
// Harvest emails/phones from home + press + contact pages (max 3 fetches beyond home).
const pagesToScan = [{ url: base, body: home.body }];
diff --git a/src/pr/jobs/index.js b/src/pr/jobs/index.js
index 94f68f54..af5132d6 100644
--- a/src/pr/jobs/index.js
+++ b/src/pr/jobs/index.js
@@ -230,6 +230,12 @@ const handlers = {
if (item.contact_form_url) patch.contact_form_url = item.contact_form_url;
if (item.general_press_email) patch.general_press_email = item.general_press_email;
if (item.general_press_phone) patch.general_press_phone = item.general_press_phone;
+ // Media/PR outreach pages (submission / editorial calendar / awards / events / media kit)
+ if (item.submission_page_url) patch.submission_page_url = item.submission_page_url;
+ if (item.editorial_calendar_url) patch.editorial_calendar_url = item.editorial_calendar_url;
+ if (item.awards_url) patch.awards_url = item.awards_url;
+ if (item.events_url) patch.events_url = item.events_url;
+ if (item.media_kit_url) patch.media_kit_url = item.media_kit_url;
patch.internal_pr_status = item.press_emails.length || item.press_page_url ? 'has_internal_pr' : org.internal_pr_status;
await organizations.update(orgId, patch, 'system:verify-organization');
for (const ev of item.evidence || []) {
diff --git a/src/pr/lib/taxonomy.js b/src/pr/lib/taxonomy.js
index ace20600..a46021ca 100644
--- a/src/pr/lib/taxonomy.js
+++ b/src/pr/lib/taxonomy.js
@@ -76,6 +76,27 @@ const ASSET_CLASSES = [
// ── Contact roles: exact-title patterns → normalized role + department. ──────
// Order matters: first match wins; more specific patterns first.
const ROLE_RULES = [
+ // Editorial & newsroom (publications, trade media, association media) — checked first
+ // so a publication's own staff classify as editorial, not fall through to leadership.
+ [/editor[\s-]?in[\s-]?chief|editor in chief/i, 'editor_in_chief', 'editorial'],
+ [/executive editor/i, 'executive_editor', 'editorial'],
+ [/managing editor/i, 'managing_editor', 'editorial'],
+ [/editorial director|director[^a-z]*(of )?editorial/i, 'editorial_director', 'editorial'],
+ [/assignment editor|news editor/i, 'news_editor', 'editorial'],
+ [/newsletter editor/i, 'newsletter_editor', 'editorial'],
+ [/(web|digital|online) editor/i, 'digital_editor', 'editorial'],
+ [/(deputy|senior|associate|assistant) editor/i, 'senior_editor', 'editorial'],
+ [/contributing editor|columnist|contributor/i, 'contributing_editor', 'editorial'],
+ [/(senior|staff)[^a-z]*(reporter|writer)/i, 'staff_writer', 'editorial'],
+ [/reporter|journalist|correspondent/i, 'reporter', 'editorial'],
+ [/copy editor/i, 'copy_editor', 'editorial'],
+ [/(^|[^a-z])editor(\b|s\b)/i, 'editor', 'editorial'],
+ // Media business side (publications) — publisher, events, awards, sponsored/branded content, ad partnerships
+ [/associate publisher|publisher/i, 'publisher', 'media_business'],
+ [/(conference|events?)[^a-z]*producer|event producer/i, 'event_producer', 'media_business'],
+ [/awards?[^a-z]*(program|manager|coordinator|director|lead)/i, 'awards_program', 'media_business'],
+ [/(sponsored|branded|custom)[^a-z]*content|content studio/i, 'sponsored_content', 'media_business'],
+ [/(advertising|ad sales|sponsorship)[^a-z]*(director|manager|sales|lead)?|media (sales|partnerships?)/i, 'advertising_partnerships', 'media_business'],
// Communications & PR
[/chief communications officer|cco\b/i, 'chief_communications_officer', 'communications'],
[/head of communications/i, 'head_of_communications', 'communications'],
@@ -170,10 +191,13 @@ function classifyTitle(exactTitle) {
// Role relevance for outreach-priority scoring (0..100): comms/PR first, then marketing,
// then BD/agency, then leadership, then lending/title contacts.
-const DEPT_RELEVANCE = { communications: 100, agency: 92, marketing: 85, bd: 72, leadership: 65, lending: 60, title_escrow: 60, other: 30 };
+// editorial = the story-pitch target at a publication (editor/reporter) — highest value.
+// media_business = publisher/events/awards/sponsored-content/ad-partnerships outreach.
+const DEPT_RELEVANCE = { editorial: 100, communications: 100, agency: 92, marketing: 85, media_business: 82, bd: 72, leadership: 65, lending: 60, title_escrow: 60, other: 30 };
function roleRelevance(department, normalized_role) {
let base = DEPT_RELEVANCE[department] != null ? DEPT_RELEVANCE[department] : 30;
if (/press_officer|media_relations|public_relations|director_communications|head_of_communications|chief_communications/.test(normalized_role || '')) base = 100;
+ if (/editor_in_chief|managing_editor|executive_editor|editorial_director|news_editor|reporter|staff_writer/.test(normalized_role || '')) base = 100;
return base;
}
diff --git a/src/pr/migrations/004_media_pr_fields.sql b/src/pr/migrations/004_media_pr_fields.sql
new file mode 100644
index 00000000..52e7d590
--- /dev/null
+++ b/src/pr/migrations/004_media_pr_fields.sql
@@ -0,0 +1,16 @@
+-- CRE PR Intelligence — media/PR-outreach fields.
+-- Additive only. Adds the publication/association-media outreach URLs that the media
+-- expansion (national CRE publications, trade media, association media) needs so the
+-- data model matches the required field list: submission page (press-release
+-- instructions), editorial calendar (planned topics), awards (nomination
+-- opportunities), events (conferences), and media kit. All nullable; populated by the
+-- verify-organization job from each org's own live site, and shown in the admin UI.
+
+ALTER TABLE pr_organizations ADD COLUMN IF NOT EXISTS submission_page_url text; -- press-release / news-tip / contribute page
+ALTER TABLE pr_organizations ADD COLUMN IF NOT EXISTS editorial_calendar_url text; -- planned topics / media planner
+ALTER TABLE pr_organizations ADD COLUMN IF NOT EXISTS awards_url text; -- awards / rankings / nomination page
+ALTER TABLE pr_organizations ADD COLUMN IF NOT EXISTS events_url text; -- conferences / webinars / event calendar
+ALTER TABLE pr_organizations ADD COLUMN IF NOT EXISTS media_kit_url text; -- media kit / advertising & partnerships
+
+-- 'editorial' is a first-class department for people at publications/association media
+-- (editors, reporters, publishers). No enum to alter — department is free text.
diff --git a/src/pr/seed/national-media-seed.js b/src/pr/seed/national-media-seed.js
new file mode 100644
index 00000000..ea68bacb
--- /dev/null
+++ b/src/pr/seed/national-media-seed.js
@@ -0,0 +1,196 @@
+'use strict';
+// National CRE MEDIA + ASSOCIATION-MEDIA + PR-agency seed — HONEST BY CONSTRUCTION.
+//
+// Adds the commercial-real-estate news publications, trade/vertical media, regional
+// business journals, industry-association media, and national CRE PR agencies to the
+// PR-intelligence database as `discovered` candidates. Each then gets a
+// verify-organization job that fetches the LIVE site to confirm it and extract the
+// real per-org fields the request asks for:
+// contact name/title/email/LinkedIn → discover-people (org's own team/press pages)
+// website / press page / newsroom → verify-organization
+// submission page / editorial calendar / awards / events / media kit → verify-organization
+// (PAGE_HINTS in adapters/website.js)
+// market / property sectors → state_presence + metros + asset_classes (below)
+// last verified → last_verified_at (set by verify)
+//
+// We seed ORGANIZATIONS only. Editors, reporters, publishers, assignment/newsletter
+// editors, sponsored-content and ad-partnership contacts are PEOPLE — they are extracted
+// from each org's own published pages by the pipeline (never hand-fabricated here).
+// In-house communications departments surface as internal_pr_status + those people.
+//
+// Idempotent: exact domain matches union geography instead of duplicating; same-apex
+// network brands (ACBJ business journals, France Media regional editions) are modeled as
+// one org with the editions documented in notes rather than colliding needs_review rows.
+//
+// Run: npm run pr:seed:national (then: npm run pr:worker)
+const db = require('../db');
+const organizations = require('../services/organizations');
+const jobs = require('../jobs');
+
+const US = ['US'];
+const NAT = ['national'];
+
+// Defaults: states=US, metros=national, counties=[], assets=[], tags=[].
+// { name, domain, type, subtype?, states?, metros?, counties?, assets?, internal_pr?, tags?, notes? }
+const ORGS = [
+ // ── National CRE trade publications ────────────────────────────────────────
+ { name: 'RENTV', domain: 'rentv.com', type: 'trade_publication', subtype: 'cre_broadcast_publication',
+ tags: ['first_party'], notes: 'RENTV network (first-party; included for completeness of the media map).' },
+ { name: 'Commercial Property Executive', domain: 'cpexecutive.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['office', 'industrial', 'retail', 'multifamily'], notes: 'CPE (Yardi/CommercialEdge). Editorial now at commercialsearch.com/news.' },
+ { name: 'CoStar News', domain: 'costar.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'CoStar News = costar.com/news (paywalled data + news).' },
+ { name: 'Commercial Observer', domain: 'commercialobserver.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['office', 'multifamily'], notes: 'NYC-based; national CRE + finance coverage.' },
+ { name: 'Bisnow', domain: 'bisnow.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'National event + newsletter network; many metro editions (LA, OC, SoCal, etc.).' },
+ { name: 'GlobeSt.com', domain: 'globest.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'ALM; national CRE news + events (RealShare).' },
+ { name: 'The Real Deal', domain: 'therealdeal.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'National real estate news (CRE + resi development).' },
+ { name: 'Connect CRE', domain: 'connectcre.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'National + regional CRE news (California, etc.).' },
+ { name: 'REBusinessOnline', domain: 'rebusinessonline.com', type: 'trade_publication', subtype: 'cre_publication',
+ notes: 'France Media Inc. Regional print editions: Western, Heartland, Northeast, Southeast, Texas Real Estate Business + California Centers. Distinct-domain siblings: Shopping Center Business, Student Housing Business, Seniors Housing Business.' },
+ { name: 'Multi-Housing News', domain: 'multihousingnews.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['multifamily', 'affordable_housing'], notes: 'Yardi/CommercialEdge; multifamily.' },
+ { name: 'National Real Estate Investor', domain: 'nreionline.com', type: 'trade_publication', subtype: 'cre_publication',
+ tags: ['archived_brand'], notes: 'Legacy NREI brand; content merged into WealthManagement.com (Informa). May redirect.' },
+ { name: 'Shopping Center Business', domain: 'shoppingcenterbusiness.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['retail'], notes: 'France Media; retail / shopping-center CRE. Regional retail sibling: California Centers.' },
+ { name: 'Student Housing Business', domain: 'studenthousingbusiness.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['student_housing'], notes: 'France Media; student housing.' },
+ { name: 'Seniors Housing Business', domain: 'seniorshousingbusiness.com', type: 'trade_publication', subtype: 'cre_publication',
+ assets: ['senior_housing'], notes: 'France Media; seniors housing.' },
+ { name: 'Propmodo', domain: 'propmodo.com', type: 'trade_publication', subtype: 'proptech_media',
+ notes: 'Proptech + built-world media and research.' },
+ { name: 'CREtech', domain: 'cretech.com', type: 'trade_publication', subtype: 'proptech_media',
+ notes: 'CRE technology news + events.' },
+ { name: 'Blueprint', domain: 'blueprintvegas.com', type: 'trade_publication', subtype: 'proptech_events',
+ tags: ['domain_unverified'], notes: 'Proptech / real-estate innovation conference & media (Blueprint).' },
+ { name: 'The Registry', domain: 'theregistrysf.com', type: 'trade_publication', subtype: 'cre_publication',
+ states: US.concat('CA'), metros: ['bay-area'], notes: 'NorCal (SF Bay) + Pacific NW CRE.' },
+
+ // ── Vertical / sector trade media ──────────────────────────────────────────
+ { name: 'Urban Land', domain: 'urbanland.uli.org', type: 'trade_publication', subtype: 'association_magazine',
+ notes: 'ULI’s magazine (Urban Land). Parent: Urban Land Institute (uli.org).' },
+ { name: 'Building Design+Construction', domain: 'bdcnetwork.com', type: 'trade_publication', subtype: 'aec_media',
+ assets: ['office', 'mixed_use'], notes: 'AEC — building design + construction.' },
+ { name: 'Engineering News-Record', domain: 'enr.com', type: 'trade_publication', subtype: 'construction_media',
+ notes: 'ENR — construction industry news + rankings (Top 400 Contractors).' },
+ { name: 'Construction Dive', domain: 'constructiondive.com', type: 'trade_publication', subtype: 'construction_media',
+ notes: 'Industry Dive; construction news.' },
+ { name: 'Hotel Business', domain: 'hotelbusiness.com', type: 'trade_publication', subtype: 'hospitality_media',
+ assets: ['hospitality'], notes: 'ICD Publications; hotel industry.' },
+ { name: 'Hotel Management', domain: 'hotelmanagement.net', type: 'trade_publication', subtype: 'hospitality_media',
+ assets: ['hospitality'], notes: 'Questex; hotel operations/development.' },
+ { name: 'Hospitality Design', domain: 'hospitalitydesign.com', type: 'trade_publication', subtype: 'hospitality_media',
+ assets: ['hospitality'], notes: 'Emerald; hospitality design (HD).' },
+ { name: 'Senior Housing News', domain: 'seniorhousingnews.com', type: 'trade_publication', subtype: 'seniors_media',
+ assets: ['senior_housing'], notes: 'Aging Media Network.' },
+ { name: 'Affordable Housing Finance', domain: 'housingfinance.com', type: 'trade_publication', subtype: 'finance_media',
+ assets: ['affordable_housing'], notes: 'Zonda; AHF.' },
+ { name: 'Mortgage Professional America', domain: 'mpamag.com', type: 'trade_publication', subtype: 'finance_media',
+ notes: 'KM Business Information; mortgage industry (MPA).' },
+ { name: 'Scotsman Guide', domain: 'scotsmanguide.com', type: 'trade_publication', subtype: 'finance_media',
+ notes: 'Commercial + residential mortgage lending media (Commercial edition).' },
+ { name: 'National Mortgage Professional', domain: 'nationalmortgageprofessional.com', type: 'trade_publication', subtype: 'finance_media',
+ notes: 'NMP Media; mortgage industry.' },
+
+ // ── Regional business journals (real-estate sections) ──────────────────────
+ { name: 'American City Business Journals', domain: 'bizjournals.com', type: 'trade_publication', subtype: 'regional_business_journal_network',
+ states: US.concat(['CA', 'AZ', 'CO', 'TX', 'FL']), metros: ['bay-area', 'sacramento'],
+ notes: 'ACBJ network — CRE sections at bizjournals.com/<city>/news/commercial-real-estate. Editions requested: San Francisco Business Times, Phoenix, Sacramento, Denver, Dallas, Houston, South Florida Business Journals. Per-market CRE reporters are the outreach targets.' },
+ { name: 'Orange County Business Journal', domain: 'ocbj.com', type: 'trade_publication', subtype: 'regional_business_journal',
+ states: US.concat('CA'), metros: ['oc'], counties: ['Orange'], notes: 'Independent (not ACBJ). Real estate section.' },
+ // (Los Angeles Business Journal + San Diego Business Journal already seeded in ca-seed.)
+
+ // ── Industry & association media ───────────────────────────────────────────
+ { name: 'Urban Land Institute', domain: 'uli.org', type: 'association', subtype: 'industry_association',
+ assets: ['mixed_use', 'office', 'multifamily'], notes: 'ULI. Media: Urban Land magazine (urbanland.uli.org). Awards + fall/spring meetings.' },
+ { name: 'NAIOP', domain: 'naiop.org', type: 'association', subtype: 'industry_association',
+ assets: ['office', 'industrial', 'logistics'], notes: 'NAIOP (Commercial Real Estate Development Assn). Media: Development magazine. Awards + CRE.Converge.' },
+ { name: 'ICSC', domain: 'icsc.com', type: 'association', subtype: 'industry_association',
+ assets: ['retail'], notes: 'ICSC. Media: Commerce + Communities Today (CCT). Events: ICSC LAS VEGAS + regional.' },
+ { name: 'BOMA International', domain: 'boma.org', type: 'association', subtype: 'industry_association',
+ assets: ['office'], notes: 'BOMA. Media: BOMA Magazine. TOBY Awards + annual conference.' },
+ { name: 'CCIM Institute', domain: 'ccim.com', type: 'association', subtype: 'industry_association',
+ notes: 'CCIM. Media: Commercial Investment Real Estate (CIRE) magazine.' },
+ { name: 'SIOR', domain: 'sior.com', type: 'association', subtype: 'industry_association',
+ assets: ['industrial', 'office'], notes: 'Society of Industrial and Office Realtors. SIOR Report magazine.' },
+ { name: 'NMHC', domain: 'nmhc.org', type: 'association', subtype: 'industry_association',
+ assets: ['multifamily'], notes: 'National Multifamily Housing Council. Annual meeting, OpTech.' },
+ { name: 'National Apartment Association', domain: 'naahq.org', type: 'association', subtype: 'industry_association',
+ assets: ['multifamily'], notes: 'NAA. units magazine. Apartmentalize event.' },
+ { name: 'Mortgage Bankers Association', domain: 'mba.org', type: 'association', subtype: 'finance_association',
+ notes: 'MBA. CREF/Multifamily Convention. MBA NewsLink.' },
+ { name: 'CRE Finance Council', domain: 'crefc.org', type: 'association', subtype: 'finance_association',
+ notes: 'CREFC. Conferences (January, June, Miami).' },
+ { name: 'CoreNet Global', domain: 'corenetglobal.org', type: 'association', subtype: 'industry_association',
+ assets: ['office'], notes: 'Corporate real estate. LEADER magazine. Global Summit.' },
+ { name: 'American Institute of Architects', domain: 'aia.org', type: 'association', subtype: 'design_association',
+ notes: 'AIA. Architect magazine + AIA Conference on Architecture.' },
+ { name: 'American Society of Landscape Architects', domain: 'asla.org', type: 'association', subtype: 'design_association',
+ notes: 'ASLA. Landscape Architecture Magazine (LAM).' },
+ { name: 'U.S. Green Building Council', domain: 'usgbc.org', type: 'association', subtype: 'sustainability_association',
+ notes: 'USGBC (LEED). Greenbuild conference + media.' },
+ { name: 'Construction Management Association of America', domain: 'cmaanet.org', type: 'association', subtype: 'construction_association',
+ notes: 'CMAA.' },
+
+ // ── National CRE PR / communications agencies ──────────────────────────────
+ { name: 'Great Ink Communications', domain: 'greatink.com', type: 'pr_agency', notes: 'NYC CRE-specialist PR.' },
+ { name: 'Marino', domain: 'marinopr.com', type: 'pr_agency', notes: 'CRE + real estate PR (NY).' },
+ { name: 'BerlinRosen', domain: 'berlinrosen.com', type: 'pr_agency', notes: 'National PR; real estate practice.' },
+ { name: 'Rubenstein', domain: 'rubenstein.com', type: 'pr_agency', notes: 'Real estate PR (NY).' },
+ { name: 'Beckerman PR', domain: 'beckermanpr.com', type: 'pr_agency', notes: 'CRE / real estate PR (NJ/NY).' },
+ { name: 'Lou Hammond Group', domain: 'louhammond.com', type: 'pr_agency', notes: 'Hospitality + real estate PR.' },
+ { name: 'Gregory FCA', domain: 'gregoryfca.com', type: 'pr_agency', notes: 'Financial + real estate PR.' },
+];
+
+async function main() {
+ const h = await db.health();
+ if (!h.ok) { console.error('DB unavailable: ' + h.reason); process.exit(1); }
+ await db.runMigrations({ log: (m) => console.log('[migrate] ' + m) });
+ let created = 0, matched = 0, verifyJobs = 0;
+ for (const o of ORGS) {
+ const r = await organizations.create({
+ display_name: o.name,
+ website_url: 'https://www.' + o.domain,
+ organization_type: o.type,
+ organization_subtype: o.subtype || null,
+ asset_classes: o.assets || [],
+ state_presence: o.states || US,
+ metros: o.metros || NAT,
+ counties: o.counties || [],
+ internal_pr_status: o.internal_pr || 'unknown',
+ tags: (o.tags || []).concat('media-map'),
+ notes: o.notes || null,
+ lifecycle_status: 'discovered',
+ }, {
+ evidence: {
+ source: {
+ source_type: 'manual',
+ source_name: 'National CRE media + PR curated seed list',
+ url: 'https://www.' + o.domain,
+ short_excerpt: 'Curated candidate entry — real CRE media/association/PR organization pending live-website verification by the pipeline. Not treated as verified until verify-organization confirms against the live site.',
+ is_primary_source: false,
+ license_or_usage_note: 'Candidate list only; all facts must be re-verified against primary sources before outreach.',
+ adapter: 'seed',
+ },
+ fields: { organization: 45 },
+ },
+ actor: 'system:national-media-seed',
+ });
+ if (r.created) created++; else matched++;
+ const j = await jobs.enqueue('verify-organization', { organization_id: r.org.id }, { priority: 5, actor: 'system:national-media-seed' });
+ if (!j.deduped) verifyJobs++;
+ }
+ await jobs.enqueue('detect-duplicates', {}, { priority: 8, actor: 'system:national-media-seed' });
+ await jobs.enqueue('calculate-scores', {}, { priority: 9, actor: 'system:national-media-seed' });
+ console.log(`[national-media-seed] curated candidates: ${ORGS.length} (created ${created}, matched-existing ${matched})`);
+ console.log(`[national-media-seed] verify-organization jobs queued: ${verifyJobs}`);
+ console.log('[national-media-seed] run the worker to verify + extract editorial contacts: npm run pr:worker');
+ process.exit(0);
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/src/pr/services/organizations.js b/src/pr/services/organizations.js
index c6d834d4..8934d426 100644
--- a/src/pr/services/organizations.js
+++ b/src/pr/services/organizations.js
@@ -72,8 +72,9 @@ async function create(data, { evidence, actor } = {}) {
organization_type, organization_subtype, asset_classes, headquarters, state_presence,
metros, counties, office_addresses, parent_organization_id, internal_pr_status,
press_page_url, newsroom_url, general_press_email, general_press_phone, contact_form_url,
- verification_status, lifecycle_status, notes, tags, import_batch_id)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26)
+ verification_status, lifecycle_status, notes, tags, import_batch_id,
+ submission_page_url, editorial_calendar_url, awards_url, events_url, media_kit_url)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31)
RETURNING *`,
[clean(data.legal_name, 300) || null, display_name, normalized_name, domain,
clean(data.website_url, 500) || null, clean(data.linkedin_company_url, 500) || null,
@@ -87,7 +88,10 @@ async function create(data, { evidence, actor } = {}) {
clean(data.contact_form_url, 500) || null,
data.verification_status || 'unverified',
uncertain.length ? 'needs_review' : (data.lifecycle_status || 'discovered'),
- clean(data.notes, 4000) || null, data.tags || [], data.import_batch_id || null]);
+ clean(data.notes, 4000) || null, data.tags || [], data.import_batch_id || null,
+ clean(data.submission_page_url, 500) || null, clean(data.editorial_calendar_url, 500) || null,
+ clean(data.awards_url, 500) || null, clean(data.events_url, 500) || null,
+ clean(data.media_kit_url, 500) || null]);
const org = r.rows[0];
if (evidence && evidence.source) {
const src = await sources.findOrCreateSource(evidence.source, client);
@@ -188,7 +192,8 @@ async function update(id, patch, actor) {
'organization_subtype', 'asset_classes', 'headquarters', 'state_presence', 'metros', 'counties',
'office_addresses', 'parent_organization_id', 'internal_pr_status', 'press_page_url', 'newsroom_url',
'general_press_email', 'general_press_phone', 'contact_form_url', 'verification_status',
- 'lifecycle_status', 'notes', 'tags', 'last_verified_at'];
+ 'lifecycle_status', 'notes', 'tags', 'last_verified_at',
+ 'submission_page_url', 'editorial_calendar_url', 'awards_url', 'events_url', 'media_kit_url'];
const sets = []; const params = [];
for (const k of allowed) {
if (patch[k] === undefined) continue;
← 955211ec RENTV: deals+registry ride the 20-min news cron (auto-accumu
·
back to Rentv
·
deploy: exclude data/deals-registry.json so deploys never cl 06fe8285 →