← 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
A lib/firms.jsM routes/public.jsA views/public/firm.ejsA views/public/firms.ejs
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,'"') %>" 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 →