[object Object]

← back to Stars of Design

feat(starsofdesign): /firms tier — listFirms+getFirm lib, /firms+/firms/:slug routes, firms.ejs + firm.ejs views

adfe6edbd67eb94c20e81fa4039646223d389b7c · 2026-05-12 15:21:38 -0700 · Steve Abrams

Files touched

Diff

commit adfe6edbd67eb94c20e81fa4039646223d389b7c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 15:21:38 2026 -0700

    feat(starsofdesign): /firms tier — listFirms+getFirm lib, /firms+/firms/:slug routes, firms.ejs + firm.ejs views
---
 lib/firms.js           | 107 +++++++++++++++++++++++++++++++++++++++++++++++++
 routes/public.js       |  37 +++++++++++++++++
 views/public/firm.ejs  |  91 +++++++++++++++++++++++++++++++++++++++++
 views/public/firms.ejs | 104 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 339 insertions(+)

diff --git a/lib/firms.js b/lib/firms.js
new file mode 100644
index 0000000..990d79b
--- /dev/null
+++ b/lib/firms.js
@@ -0,0 +1,107 @@
+'use strict';
+// Firm-tier listings for /firms — surfaces the seeded top-US firms (Gensler,
+// Studio Sofield, Kelly Wearstler etc.) plus any firms auto-materialized from
+// sig-extract. Independent from /clients which keys on designer rows.
+//
+// PII policy: city + state + areas only. No street, no zip, no phone. Same as
+// /clients.
+
+const { Pool } = require('pg');
+
+const pool = new Pool({
+  connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp&user=stevestudio2',
+  max: 4,
+});
+
+async function listFirms({ q = null, limit = 300 } = {}) {
+  const params = [];
+  let where = `1=1`;
+  if (q) {
+    params.push(`%${q.toLowerCase()}%`);
+    where += ` AND (LOWER(f.name) LIKE $${params.length} OR LOWER(f.city) LIKE $${params.length} OR LOWER(f.state_or_region) LIKE $${params.length})`;
+  }
+  params.push(limit);
+  const sql = `
+    SELECT f.id, f.slug, f.name, f.city, f.state_or_region AS state, f.country,
+           f.founded_year, f.size_band, f.website, f.bio, f.styles,
+           f.areas_served, f.logo_source, f.logo_url, f.claimed_at,
+           (SELECT count(*) FROM sod_designer_firm df WHERE df.firm_id = f.id AND df.is_current=true) AS designer_count,
+           (
+             SELECT json_agg(json_build_object('url', l.url, 'kind', l.kind))
+               FROM sod_links l
+              WHERE l.firm_id = f.id
+           ) AS links
+      FROM sod_firms f
+     WHERE ${where}
+     ORDER BY (f.claimed_at IS NOT NULL) DESC,
+              f.size_band DESC NULLS LAST,
+              f.name
+     LIMIT $${params.length}`;
+  const r = await pool.query(sql, params);
+  return r.rows.map(row => {
+    const linksByKind = {};
+    for (const l of (row.links || [])) {
+      (linksByKind[l.kind] = linksByKind[l.kind] || []).push(l.url);
+    }
+    return {
+      id: row.id,
+      slug: row.slug,
+      name: row.name,
+      city: row.city,
+      state: row.state,
+      country: row.country,
+      founded_year: row.founded_year,
+      size_band: row.size_band,
+      website: row.website,
+      bio: row.bio,
+      styles: row.styles || [],
+      areas: row.areas_served || [],
+      designer_count: parseInt(row.designer_count, 10) || 0,
+      claimed: !!row.claimed_at,
+      logo: (row.logo_source === 'made-with-ai' || !row.logo_url) ? '/img/made-with-ai.svg' : row.logo_url,
+      links: linksByKind,
+    };
+  });
+}
+
+async function getFirm(slug) {
+  const r = await pool.query(
+    `SELECT f.*,
+            (SELECT count(*) FROM sod_designer_firm df WHERE df.firm_id = f.id AND df.is_current=true) AS designer_count
+       FROM sod_firms f
+      WHERE f.slug = $1
+      LIMIT 1`, [slug]);
+  if (!r.rows[0]) return null;
+  const f = r.rows[0];
+  const dr = await pool.query(
+    `SELECT d.id, d.slug, d.full_name, d.role, d.city, d.state_or_region AS state
+       FROM sod_designers d
+       JOIN sod_designer_firm df ON df.designer_id = d.id AND df.is_current = true
+      WHERE df.firm_id = $1
+      ORDER BY d.full_name
+      LIMIT 50`, [f.id]);
+  const lr = await pool.query(
+    `SELECT url, kind FROM sod_links WHERE firm_id = $1`, [f.id]);
+  const linksByKind = {};
+  for (const l of lr.rows) (linksByKind[l.kind] = linksByKind[l.kind] || []).push(l.url);
+  return {
+    id: f.id, slug: f.slug, name: f.name,
+    city: f.city, state: f.state_or_region, country: f.country,
+    founded_year: f.founded_year, size_band: f.size_band,
+    website: f.website, bio: f.bio,
+    styles: f.styles || [],
+    areas: f.areas_served || [],
+    designer_count: parseInt(f.designer_count, 10) || 0,
+    claimed: !!f.claimed_at,
+    logo: (f.logo_source === 'made-with-ai' || !f.logo_url) ? '/img/made-with-ai.svg' : f.logo_url,
+    links: linksByKind,
+    designers: dr.rows,
+  };
+}
+
+async function totalCount() {
+  const r = await pool.query(`SELECT count(*) AS n FROM sod_firms`);
+  return parseInt(r.rows[0].n, 10);
+}
+
+module.exports = { listFirms, getFirm, totalCount };
diff --git a/routes/public.js b/routes/public.js
index c618492..260b56f 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -3,6 +3,7 @@ const router = express.Router();
 
 const data = require('../lib/data');
 const clientsLib = require('../lib/clients');
+const firmsLib = require('../lib/firms');
 const feedLib = require('../lib/feed');
 const { sortDesigners, SORTS } = require('../lib/sort');
 
@@ -109,6 +110,42 @@ router.get('/clients/:slug', async (req, res, next) => {
   } catch (e) { next(e); }
 });
 
+// ── Firm tier — seeded top-US architecture + interior design firms
+// (Gensler, Studio Sofield, Kelly Wearstler, ...) plus any firms
+// auto-materialized from sig-extract. Keyed on sod_firms not sod_designers.
+router.get('/firms', async (req, res, next) => {
+  try {
+    const q = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q : null;
+    const [firms, total] = await Promise.all([
+      firmsLib.listFirms({ q, limit: 400 }),
+      firmsLib.totalCount(),
+    ]);
+    res.render('public/firms', {
+      title: 'Top US Design Firms — Architecture + Interiors | Stars of Design',
+      meta_desc_override: 'Top US architecture and interior design firms — Gensler, HOK, Perkins&Will, Studio Sofield, Kelly Wearstler, Studio McGee, and more. Public-data seed; firms may claim and edit their profile.',
+      firms,
+      total,
+      filter: { q },
+    });
+  } catch (e) { next(e); }
+});
+
+router.get('/firms/:slug', async (req, res, next) => {
+  try {
+    const slug = req.params.slug;
+    if (!/^[a-z0-9-]{1,120}$/.test(slug)) {
+      return res.status(404).render('public/404', { title: 'Not found — Stars of Design' });
+    }
+    const f = await firmsLib.getFirm(slug);
+    if (!f) return res.status(404).render('public/404', { title: 'Not found — Stars of Design' });
+    res.render('public/firm', {
+      title: `${f.name} — Design Firm | Stars of Design`,
+      meta_desc_override: `${f.name}${f.city ? ' · ' + f.city : ''}${f.state ? ', ' + f.state : ''}${f.founded_year ? ' · founded ' + f.founded_year : ''}.`,
+      firm: f,
+    });
+  } catch (e) { next(e); }
+});
+
 // ── Phone-shaped social feed for the /clients tier — Facebook/X.com vibe.
 // Anonymous likes via httpOnly=false cookie (so client JS can show liked-state
 // optimistically). Anonymous comments require a name + body; we collect
diff --git a/views/public/firm.ejs b/views/public/firm.ejs
new file mode 100644
index 0000000..5049140
--- /dev/null
+++ b/views/public/firm.ejs
@@ -0,0 +1,91 @@
+<%- include('../partials/head', { title: title, meta_desc_override: typeof meta_desc_override !== 'undefined' ? meta_desc_override : undefined }) %>
+<%- include('../partials/header') %>
+
+<main class="designer">
+  <article class="profile wrap">
+
+    <header class="profile-head">
+      <div class="profile-photo">
+        <img src="<%= firm.logo %>" alt="<%= firm.name %> logo" loading="lazy" decoding="async" style="width:200px;height:200px;border-radius:12px;object-fit:contain;border:1px solid var(--line,#2a2620);padding:12px;background:#fff">
+      </div>
+      <div class="profile-meta">
+        <h1>
+          <%= firm.name %>
+          <% if (firm.claimed) { %><span class="chip gold" style="font-size:11px;letter-spacing:0.12em;margin-left:8px">VERIFIED</span><% } %>
+        </h1>
+
+        <% if (firm.city || firm.state) { %>
+          <p class="lede"><%= [firm.city, firm.state].filter(Boolean).join(', ') %></p>
+        <% } %>
+
+        <p class="muted">
+          <% if (firm.founded_year) { %>est. <%= firm.founded_year %><% } %>
+          <% if (firm.size_band) { %> · <%= firm.size_band %> people<% } %>
+          <% if (firm.designer_count > 0) { %> · <%= firm.designer_count %> principal<%= firm.designer_count === 1 ? '' : 's' %><% } %>
+        </p>
+
+        <% if (firm.styles && firm.styles.length) { %>
+          <h3 style="font-size:11px;letter-spacing:0.18em;text-transform:uppercase;color:var(--ink-faint,#7a706a);margin:24px 0 8px">Practice areas</h3>
+          <ul class="tags">
+            <% firm.styles.forEach(function(s) { %><li><%= s %></li><% }); %>
+          </ul>
+        <% } %>
+
+        <% if (firm.areas && firm.areas.length) { %>
+          <h3 style="font-size:11px;letter-spacing:0.18em;text-transform:uppercase;color:var(--ink-faint,#7a706a);margin:24px 0 8px">Geographic reach</h3>
+          <ul class="tags">
+            <% firm.areas.forEach(function(a) { %><li><%= a %></li><% }); %>
+          </ul>
+        <% } %>
+
+        <% var hasLinks = firm.links && (firm.links.linkedin || firm.links['firm-site'] || firm.links.instagram); %>
+        <% if (hasLinks) { %>
+          <h3 style="font-size:11px;letter-spacing:0.18em;text-transform:uppercase;color:var(--ink-faint,#7a706a);margin:24px 0 8px">Links</h3>
+          <ul class="link-list" style="list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:12px">
+            <% (firm.links['firm-site'] || []).slice(0,2).forEach(function(u) { %>
+              <li><a href="<%= u %>" rel="nofollow noopener" target="_blank">Website ↗</a></li>
+            <% }); %>
+            <% (firm.links.linkedin || []).slice(0,2).forEach(function(u) { %>
+              <li><a href="<%= u %>" rel="nofollow noopener" target="_blank">LinkedIn ↗</a></li>
+            <% }); %>
+            <% (firm.links.instagram || []).slice(0,2).forEach(function(u) { %>
+              <li><a href="<%= u %>" rel="nofollow noopener" target="_blank">Instagram ↗</a></li>
+            <% }); %>
+          </ul>
+        <% } %>
+
+        <% if (!firm.claimed) { %>
+          <p style="margin-top:32px">
+            <a href="/submit" class="btn primary">Claim this firm →</a>
+          </p>
+        <% } %>
+      </div>
+    </header>
+
+    <% if (firm.bio) { %>
+      <section style="margin-top:48px">
+        <h2 class="muted" style="font-size:11px;letter-spacing:0.18em;text-transform:uppercase">About</h2>
+        <p class="prose"><%= firm.bio %></p>
+      </section>
+    <% } %>
+
+    <% if (firm.designers && firm.designers.length) { %>
+      <section style="margin-top:48px">
+        <h2 class="muted" style="font-size:11px;letter-spacing:0.18em;text-transform:uppercase">Principals</h2>
+        <ul class="link-list" style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px">
+          <% firm.designers.forEach(function(d) { %>
+            <li>
+              <a href="/clients/<%= d.slug %>"><strong><%= d.full_name %></strong></a>
+              <% if (d.role) { %> <span class="muted">· <%= d.role %></span><% } %>
+              <% if (d.city || d.state) { %> <span class="muted">· <%= [d.city, d.state].filter(Boolean).join(', ') %></span><% } %>
+            </li>
+          <% }); %>
+        </ul>
+      </section>
+    <% } %>
+
+    <p style="margin-top:64px"><a href="/firms" class="muted">← Back to all firms</a></p>
+  </article>
+</main>
+
+<%- include('../partials/footer') %>
diff --git a/views/public/firms.ejs b/views/public/firms.ejs
new file mode 100644
index 0000000..9a032c7
--- /dev/null
+++ b/views/public/firms.ejs
@@ -0,0 +1,104 @@
+<%- include('../partials/head', { title: 'Top US Design Firms · Stars of Design' }) %>
+<%- include('../partials/header') %>
+
+<main class="home">
+  <section class="hero-gucci" aria-labelledby="hero-wordmark" style="min-height:42vh">
+    <div class="hero-gucci-bg" role="img" aria-label="Editorial firm placeholder"></div>
+    <div class="hero-gucci-stack">
+      <h1 id="hero-wordmark" class="hero-gucci-wordmark" style="font-size:clamp(40px,5vw,72px)">Top US Firms</h1>
+      <p class="hero-gucci-tagline"><em>Architecture + interior design — the public-knowledge canon, seeded.</em></p>
+      <div class="hero-gucci-cta">
+        <a class="btn ghost" href="/clients">DW client designers →</a>
+        <a class="btn ghost" href="/designers" style="margin-left:8px">Editorial directory →</a>
+      </div>
+    </div>
+  </section>
+
+  <section class="section" id="firms">
+    <div class="wrap">
+      <div class="section-head">
+        <h2>Architecture + interior design firms</h2>
+        <span class="muted"><%= firms.length %> of <%= total %> · public-knowledge seed + sig-extract</span>
+      </div>
+
+      <form class="grid-controls" method="get" action="/firms" data-grid-controls>
+        <div class="grid-controls-left">
+          <input type="search" name="q" placeholder="Search firm, city, state" value="<%= (filter.q||'').replace(/"/g,'&quot;') %>" style="background:transparent;border:1px solid var(--line,#2a2620);color:inherit;padding:7px 12px;border-radius:6px;font:inherit;min-width:240px">
+          <button type="submit" class="btn ghost" style="margin-left:8px">Search</button>
+        </div>
+        <div class="grid-controls-right">
+          <label class="control">
+            <span>Density</span>
+            <input id="density-slider" type="range" min="220" max="440" step="20" value="320" data-density>
+          </label>
+        </div>
+      </form>
+
+      <% if (!firms.length) { %>
+        <p class="muted" style="padding:60px 0;text-align:center;font-style:italic">No firms match that filter yet.</p>
+      <% } %>
+
+      <div class="designer-grid" data-designer-grid>
+        <% firms.forEach(function(f) { %>
+          <article class="designer-card">
+            <header>
+              <span class="card-mark" aria-hidden="true"><%= (f.name||'?').split(/[\s&+]/).filter(Boolean).map(s=>s[0]).join('').slice(0,2).toUpperCase() %></span>
+              <div>
+                <h3><a href="/firms/<%= f.slug %>"><%= f.name %></a><% if (f.claimed) { %> <span class="chip gold" style="margin-left:6px">CLAIMED</span><% } %></h3>
+                <p class="muted">
+                  <% if (f.city || f.state) { %><%= [f.city, f.state, f.country && f.country !== 'United States' && f.country !== 'USA' ? f.country : null].filter(Boolean).join(', ') %><% } %>
+                  <% if (f.founded_year) { %> · est. <%= f.founded_year %><% } %>
+                  <% if (f.size_band) { %> · <%= f.size_band %><% } %>
+                  <% if (f.designer_count > 0) { %> · <%= f.designer_count %> principal<%= f.designer_count === 1 ? '' : 's' %><% } %>
+                </p>
+              </div>
+            </header>
+
+            <% if (f.styles && f.styles.length) { %>
+              <ul class="tags">
+                <% f.styles.slice(0,6).forEach(function(s) { %><li><%= s %></li><% }); %>
+              </ul>
+            <% } %>
+
+            <% if (f.links && (f.links.linkedin || f.links['firm-site'])) { %>
+              <div class="card-links" style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
+                <% (f.links['firm-site']||[]).slice(0,1).forEach(function(u) { %>
+                  <a href="<%= u %>" rel="nofollow noopener" target="_blank" class="muted" style="font-size:12px">Website ↗</a>
+                <% }); %>
+                <% (f.links.linkedin||[]).slice(0,1).forEach(function(u) { %>
+                  <a href="<%= u %>" rel="nofollow noopener" target="_blank" class="muted" style="font-size:12px">LinkedIn ↗</a>
+                <% }); %>
+                <% (f.links.instagram||[]).slice(0,1).forEach(function(u) { %>
+                  <a href="<%= u %>" rel="nofollow noopener" target="_blank" class="muted" style="font-size:12px">Instagram ↗</a>
+                <% }); %>
+              </div>
+            <% } %>
+          </article>
+        <% }); %>
+      </div>
+    </div>
+  </section>
+
+  <section class="section alt">
+    <div class="wrap">
+      <h2>How this list is built</h2>
+      <p class="prose">
+        The firm canon is seeded from public knowledge — company names, founding
+        years, headquarters cities, and LinkedIn company slugs. Logos are
+        placeholder <em>made-with-ai</em> marks until the firm claims and
+        uploads a real one. <strong>Never extracted or scraped from
+        LinkedIn</strong> — every datum here is on the firm's own About page
+        or in public press.
+      </p>
+      <p class="prose">
+        Firms can <a href="/submit">claim and edit their profile</a> at any
+        time. Claimed profiles get a Verified badge, real logo upload, full
+        portfolio gallery, and the ability to list current principals.
+      </p>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>
+<script src="/js/grid-controls.js" defer></script>
+<script src="/js/hamburger.js" defer></script>

← ddd574f feat(starsofdesign): expand top-us-firms seed +39 (arch 39,  ·  back to Stars of Design  ·  feat(starsofdesign): add /firms /clients /feed to global nav 23833cd →