Stars of Design
A curated directory of the most influential interior designers + their firms + their real DW clients.
Search the build
-
06be86b2026-05-31 Add per-site favicon (kills /favicon.ico 404) -
8fc1daa2026-05-19 Static-bak guard returns plain text, not EJS template -
e2ee6aa2026-05-19 Guard against serving .bak / .pre- snapshot artifacts from public/ -
a7ac2a92026-05-19 Add noreferrer to all rel=nofollow noopener external links -
18acbe82026-05-19 Hydrate saved sort BEFORE first grid render -
e85f2382026-05-13 Makefile: audit target -
af307bc2026-05-13 smoke: /api/health perf budget 500→1000ms -
f86aef52026-05-13 polish: editorial 2-line hero + by-era anthology section -
03cea562026-05-13 smoke: assert /api/{designers,firms,clients}/featured SWR Cache-Control (3 checks) -
ea921b62026-05-13 smoke: cover /healthz + HEAD method + harness opts.method -
4817b262026-05-13 smoke: assert Last-Modified on /sitemap.{xml,txt} -
66508472026-05-13 smoke: home perf budget <1500ms -
d4b32db2026-05-13 Makefile: add help target listing all available commands -
54ab22f2026-05-13 smoke: /api/health perf budget (<500ms) -
fe4a5062026-05-13 Makefile: add fleet-status target -
bcf553b2026-05-13 smoke: assert /firms + /clients sort + density (grid standard) -
86dc3192026-05-13 smoke: assert /designers has sort + density controls (Steve's grid standard) -
73e8ba02026-05-13 smoke: 3 more home assertions — Permissions-Policy, canonical, color-scheme (asim parity) -
4be3a232026-05-13 smoke: assert X-Frame-Options on home (asim parity) -
338bd3e2026-05-13 smoke: assert HSTS + CSP headers (helmet regression guard) -
e88edf92026-05-13 smoke: assert Vary: Accept-Encoding -
3f74a862026-05-13 smoke: assert /api/version git !== 'unknown' -
d45d22b2026-05-13 smoke: assert /api/version Cache-Control no-store -
fd7960e2026-05-13 smoke: 2 assertions for branded 404 page -
95b38f52026-05-13 smoke: 4 assertions for /api/version -
49fd5af2026-05-13 api/version: deploy fingerprint endpoint (asim parity) -
90ada6d2026-05-13 smoke: uptime_s + counts.designers assertions on /api/health -
eef66c32026-05-13 api/health: include counts (designers/firms) + uptime_s + as_of (asim parity) -
3774c032026-05-13 smoke: 6 new checks for /api/{designers,firms,clients}/featured -
deb329f2026-05-13 API: add /api/{designers,firms,clients}/featured (parity with asim featured) -
ce39c6b2026-05-13 sitemap.txt: emit Last-Modified header for conditional GET (asim parity) -
4edf44d2026-05-13 smoke: 9 new checks for /api/{designers,firms,clients}/random + /random/{designer,firm,client} -
da271422026-05-13 API: add /random/designer + /random/firm + /random/client redirect helpers -
ba5954a2026-05-13 API: add /api/designers/random + /api/firms/random + /api/clients/random (parity with asim) -
d54a2572026-05-13 sitemap.xml: emit Last-Modified header for conditional GET (asim parity) -
fe83a3f2026-05-13 sod robots.txt: also reference /sitemap.txt. 54/54. -
a808c8d2026-05-13 sod Makefile — common dev targets (mirrors asim) -
6d21b222026-05-13 sod sitemap.xml + .txt Cache-Control 1h→6h + SWR 24h (mirrors asim). 54/54. -
c0ba7602026-05-13 sod /api discovery — list /api/health + smoke +2 /api/health checks. 54/54. -
fd9f8282026-05-13 sod GET /api/health — mirrors asim. SELECT 1 liveness via statsPool, no-store, JSON ok+ts. -
660a1732026-05-13 sod smoke --json suppresses header -
b90e2bb2026-05-13 sod smoke +--json flag (parity) -
a909b132026-05-13 sod .deploy.conf — point HEALTH_URL at /healthz (cheap liveness, no-store, faster than full home render). Mirrors asim. -
f3371102026-05-13 sod footer +/api link — matches asim. 52/52. -
920bdd72026-05-13 sod smoke +2 /sitemap.txt assertions. 52/52. -
9136d4a2026-05-13 sod GET /sitemap.txt — plain-text sitemap (Google+Bing supported). Also removed duplicate /healthz stub that was overshadowed. 50/50. -
ef12c2c2026-05-13 sod /healthz Cache-Control: no-store (parity with asim) -
9a655f62026-05-13 sod smoke +3 ItemList checks on /designers /firms /clients index pages. 50/50. -
43b04f52026-05-13 starsofdesign: ItemList JSON-LD on /designers + /clients index pages — top-60 each as Person ListItems. 47/47. -
25025702026-05-13 starsofdesign: /firms index ItemList JSON-LD — first 60 firms as Organization ListItems with positions. Rich-snippet eligible. 47/47. -
da983782026-05-13 sod head — +<meta name=color-scheme content="light dark"> since sod has runtime theme toggle. Form controls + scrollbars now respect the chosen theme. 47/47. -
5b0cc552026-05-13 sod robots.txt — Disallow /api/ (mirrors asim, prevents wasted crawl on JSON endpoints). -
47f55302026-05-13 sod /favicon.ico Cache-Control max-age=86400 (was 0). Mirrors asim. -
bb0e3922026-05-13 sod smoke +gzip /firms check -
5379dd02026-05-13 sod-smoke-quiet -
c9d39972026-05-13 starsofdesign: smoke +4 security-header checks (X-Content-Type-Options, Referrer-Policy, HSTS, CSP via helmet). 46/46. -
0160b3c2026-05-13 sodhz -
4682bab2026-05-12 sod-comments-cache -
571d2892026-05-12 sod-cache-stats -
0954ae82026-05-12 starsofdesign: Cache-Control public max-age=3600 on /sitemap.xml. Mirrors asim. -
13a504c2026-05-12 starsofdesign: gzip compression middleware (threshold 1KB) — mirrors asim. JSON-LD-heavy /firms /designers /clients pages drop over the wire. Smoke 42/42. -
a32feca2026-05-12 starsofdesign: smoke — add /api discovery doc check (+2 assertions). 42/42. -
6e94cda2026-05-12 starsofdesign: GET /api — JSON discovery doc for public API endpoints (stats/comments/like/comment) + PII policy notes. Mirrors asim /api pattern. -
9d16cb32026-05-12 starsofdesign: smoke test harness — test/smoke.js (zero deps, pure node) hits every public route and asserts 200/3xx + key DOM markers (canonical, WebSite/Person/Organization/BreadcrumbList JSON-LD, footer privacy/terms links). 40 checks. npm test wired. 40/40 pass on initial run. Mirrors asim smoke harness. -
00a2a602026-05-12 starsofdesign: GET /privacy + /terms — minimal legal pages (no-collect privacy notice, public-data terms, editorial-only liability). Mirrors asim. Linked from footer + sitemap. Closes parity gap. -
ed2f2d82026-05-12 starsofdesign: BreadcrumbList JSON-LD across all 3 detail page types — /firms/:slug (Home > Firms > {firm}), /designers/:slug (Home > Designers > {name}), /clients/:slug (Home > Clients > {name}). Packaged in @graph alongside entity schema. /firms also now emits employee→Person list for each associated designer. -
7348d912026-05-12 starsofdesign: home page — WebSite + Organization JSON-LD with SearchAction (urlTemplate→/designers?q=…) for Google sitelinks search-box eligibility. Closes the asim/sod parity gap on home-page schema. -
044e06c2026-05-12 starsofdesign: JSON-LD Person schema on /clients/:slug (DW client tier) — name + jobTitle + homeLocation + worksFor→Organization. Mirrors firm/designer pattern. Preserves PII rule (city/state only, no street). -
113ac142026-05-12 starsofdesign: JSON-LD schema — Organization on /firms/:slug (name/website/foundingDate/address/areaServed), Person on /designers/:slug (name/jobTitle/homeLocation/knowsAbout); injected via optional json_ld locals in head.ejs partial -
f17b2582026-05-12 starsofdesign: GET /favicon.ico fallback — returns public/favicon.svg so bots/legacy clients don't 404 on conventional path -
728ecab2026-05-12 starsofdesign: GET /api/stats — JSON counts (designers/firms/claimed/links/likes/comments/candidates) with shared module-level pool; mirrors asim /api/spotted/stats pattern; consumed by status-loop + external dashboards -
346478c2026-05-12 starsofdesign: seed data/vendor-domains.txt (~80 lines) — vendor manufacturers + transactional SaaS + DW family + free-mail + testing domains; consumed by any future audit-promote pipeline -
e00c4832026-05-12 promote-candidates: harden — restore 'status=new' filter; document why blanket is_designer promote is unsafe (LLM mis-labels Shopify/Twitter/vendor reps as designers) -
cca9c682026-05-12 starsofdesign: +40 firms (20 arch, 20 interior) — Selldorf, Steven Holl, COOKFOX, KieranTimberlake, Lake|Flato, Tod Williams Billie Tsien, Brad Ford ID, Kara Mann, Jonathan Adler, Carrier and Company, S.R. Gambrel etc. -
10f3ae02026-05-12 starsofdesign: sort dropdown on /firms + /clients (per standing sort+density rule) -
f55b3a72026-05-12 feat(starsofdesign): sitemap.xml now includes /firms/:slug × 184 + /clients/:slug × 61 + /feed (403 URLs total) -
23833cd2026-05-12 feat(starsofdesign): add /firms /clients /feed to global nav -
adfe6ed2026-05-12 feat(starsofdesign): /firms tier — listFirms+getFirm lib, /firms+/firms/:slug routes, firms.ejs + firm.ejs views -
ddd574f2026-05-12 feat(starsofdesign): expand top-us-firms seed +39 (arch 39, interior 39); strip Schumacher per vendor rule -
6f7c2dd2026-05-12 feat(starsofdesign): bring Big Red back — widget tag + CSP whitelist for :9935 -
da8a3432026-05-12 feat(starsofdesign/deep-recon-v2): --windows={v1|v2|all} for historical sweeps -
5927cb62026-05-12 feat(starsofdesign): /feed phone UI — vertical scroll, Like + Comment -
1f43d2b2026-05-12 feat(starsofdesign): regex pass extracts URLs from stored email signatures -
feeab562026-05-12 feat(starsofdesign): /clients route — surface real DW client designers in the directory -
fcd8cd82026-05-12 feat(starsofdesign): areas_served + PII guardrails + sig-extract pass 2 -
7dd41c82026-05-12 video gallery — 75 verified YouTube videos from AIA/ASID/AD/House Beautiful/Studio McGee + firm channels, cross-linked to 66 designer profiles via designer_slug, new /videos route with category+source+search filters, per-profile 'Watch X' rail, sitemap inclusion, fixed regex literal U+2028/9 EJS compile bug -
553c74a2026-05-12 security hardening: helmet (CSP/HSTS/X-Frame-Options/Referrer-Policy/Permissions-Policy/COOP), express-rate-limit (200/min/IP), JSON-LD <script> escape, javascript: URL filter, designerwallcoverings.com URL allowlist for preferred_dw, slug regex validation, q clamp to 120 chars, rel=noopener noreferrer everywhere, error message hardcoded -
a7bdb252026-05-12 feat(starsofdesign): DEEP George info@ recon (7-window × 8-template walk) -
f5688ed2026-05-12 feat(starsofdesign): signature-block LLM extraction → Jo Hempelmann shipped -
4ced5422026-05-12 expand to 150 designers — added 62 active US firms with firm websites, IG, and source credits to AD/House Beautiful/Elle Decor/Veranda/Robb Report/Luxe/New England Home; designer pages now render 'Find the firm' + 'Sources & press' blocks -
41d434d2026-05-12 feat(starsofdesign): pull DW client contacts from George info@ Gmail -
7022cb62026-05-12 expand directory: 26 → 88 designers (added historic foundational + mid-century + contemporary diverse voices, era_sort sorted) -
ecb46aa2026-05-12 deploy.conf for kamatera deploy script (port 9928 health check) -
4e7adb52026-05-12 fix(starsofdesign): vendors are NOT clients — apply blocklist, seed top-40 US firms -
51fa79b2026-05-12 gucci hero anatomy — logo UL + hamburger UR + huge wordmark/tagline hero with star-field ground (per standing rule) -
fed71832026-05-12 feat(starsofdesign/tick2): local-LLM triage on remaining candidates -
15426582026-05-12 feat(starsofdesign/tick1): Gmail + Shopify recon → 236 candidates / 44 designers / 41 firms -
bde44482026-05-12 starsofdesign tick0: PG schema + AI placeholder + scaffold -
70b79122026-05-10 add /sitemap.xml route + JSON-LD Person schema on designer detail -
aa14a022026-05-10 initial scaffold + GA gtag (G-YCT0F2Q692) + 27 designer profiles
Authors
- Steve100
Agents used
- secrets-manager1
Skills used
- /clients32
- /firms20
- /designers13
- /feed8
- /random8
- /health7
- /sitemap7
- /favicon4
- /healthz4
- /version4
- /comments4
- /featured3
- /like3
- /comment3
- /state3
- /role3
- /vendor-blocklist3
- /designer2
- /terms2
- /website2
- /stats2
- /localhost2
- /george-deep-recon2
- /studio2
- /city2
- /head1
- /something1
- /grid-controls1
- /mobile1
- /firm1
Creative ideas + design notes
8fc1daa · 2026-05-19 · Static-bak guard returns plain text, not EJS template
The previous version of this guard tried to render public/404.ejs, which in turn includes partials/head.ejs that depends on res.locals.meta_desc. That local is wired inside the publicRoutes router-level middleware — later in the stack than this guard runs — so a request for a .bak/.pre- path threw "meta_desc is not defined" and surfaced a 500 instead of a 404, defeating the point. Plain-text response sidesteps the template chain entirely. Verified with a smoke run: /test.html.bak / /test.bak / /something.pre-refactor.html all return 404, regular routes still 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e2ee6aa · 2026-05-19 · Guard against serving .bak / .pre- snapshot artifacts from public/
Refactor passes across the DW fleet sometimes leave behind .bak / .bak.<ts> / .pre-<label> snapshots next to the file they replace. If one of those slips into public/ it ships to prod as a world-readable static asset. Two-layer fix: 1. .gitignore — *.bak, *.bak.*, *.pre-* so new snapshots stop entering the tree to begin with. 2. server.js — small Express middleware in front of express.static() that 404s any URL path containing those patterns, so even an un-ignored slip can't be fetched. Uses 404 (not 403) so the existence of a snapshot can't be confirmed by path-probing. No tracked .bak / .pre-* files exist in the repo today; this is a forward-looking guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a7ac2a9 · 2026-05-19 · Add noreferrer to all rel=nofollow noopener external links
External links across /clients, /clients/:slug, /firms, /firms/:slug, and /feed previously used rel="nofollow noopener" — noreferrer was missing, which leaks the originating profile URL in the Referer header to third-party sites (LinkedIn, Instagram, firm websites) every time a visitor clicks through. Spec-canonical pairing for target="_blank" is "noopener noreferrer"; nofollow stays for SEO. All six view files updated; videos.ejs and designer.ejs already had the full triple. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18acbe8 · 2026-05-19 · Hydrate saved sort BEFORE first grid render
Per the standing rule: when a user has picked a sort in /designers or on the home grid, that preference must apply on the very first render of a fresh visit, not only after they touch the dropdown again. Server-side sorting is driven by ?sort= in the URL, so this commit makes public/js/grid-controls.js do a one-time replace() redirect to the saved sort value when the URL has no sort param yet. Guards against: - cross-page contamination — only redirects if the saved value is one of the options actually present in this page's <select> (so a "default" saved from /clients won't crash /designers). - redirect loops — gated by a sessionStorage flag. - form-driven pages — /clients and /firms wrap their <select> in a <form onchange="this.form.submit()">; the JS now defers to the form submit instead of double-navigating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f86aef5 · 2026-05-13 · polish: editorial 2-line hero + by-era anthology section
Hero — replace single-line tagline with 2-line italic editorial
headline ("The designers who set the room. / The wallcoverings that
set the tone.") plus 1-line sans subtitle naming Draper through
Wearstler. Bumps tagline color to #f0e3b8 + text-shadow for stronger
APCA on the dark hero ground.
New "By the era" section between the directory grid and the editorial
two-col. Four cards (Early 20th / Mid-Century / Late 20th /
Contemporary) each with a gold hairline rule, era blurb, 3 exemplar
designer names, and a count-deep-link to /designers?era=<bucket>.
Specific to the directory vertical — gives the homepage a real
"anthology by era" identity instead of template-fresh blocks.
Server passes eraIndex into the home view as a single in-memory
pass over data.load() — no DB hit. Hero-readability-auditor passes
APCA Lc 100/99 desktop/mobile, well above the 75 threshold.
6f7c2dd · 2026-05-12 · feat(starsofdesign): bring Big Red back — widget tag + CSP whitelist for :9935
Big Red avatar launcher was missing from StarsOfDesign + AsSeenInMovies even though the big-red server at :9935 was up and healthy. Earlier embed work only touched corkwallcovering + factory-floor. views/partials/footer.ejs — adds the <script src=:9935/widget.js> tag in retail mode before </body>. Widget self-mounts to body as a fixed lower-right launcher; goes green when chat is live. server.js helmet CSP — whitelists http://localhost:9935 + ws://localhost:9935 across script-src, img-src (avatar), connect-src (fetch + WebSocket), and frame-src (chat iframe). Drops these entries when the named tunnel bigred.agentabrams.com goes live.
da8a343 · 2026-05-12 · feat(starsofdesign/deep-recon-v2): --windows={v1|v2|all} for historical sweeps
Adds the 5y-7y and 7y-10y date windows to scrapers/george-deep-recon.js, gated by --windows=v2 (only the deep sweep) or --windows=all. Default remains the v1 windows (last-30d through 3y-5y) so prior automation behavior is unchanged. Run results (--windows=v2 --max-per-q=50): messages_scanned 350 clients_new_or_bumped 213 vendors_caught 71 no_email 66 Net new historical DW clients (2018-2021 traffic to info@) bumped sod_email_candidates 793 → 884.
5927cb6 · 2026-05-12 · feat(starsofdesign): /feed phone UI — vertical scroll, Like + Comment
Facebook / X.com-style mobile-first feed for the /clients tier. One
designer per card stacked vertically, anonymous like (cookie-tracked),
anonymous comment thread (name + body required, optional email).
Schema (idempotent CREATE TABLE):
sod_likes (designer_id, anon_id, created_at)
UNIQUE (designer_id, anon_id) — one like per visitor per post
sod_comments (designer_id, parent_id, author_name, author_email, body,
is_hidden, anon_id, ip_hash, created_at)
partial index on (designer_id, created_at DESC) WHERE NOT hidden
lib/feed.js
- ensureAnonId(req,res) — sets long-lived sod_anon cookie (12 hex,
httpOnly=false so client JS can show liked-state immediately)
- feedPage({anonId, limit, offset}) — pulls designers with embedded
like_count, comment_count, i_liked (boolean per caller), grouped
links. Ordered by created_at DESC (newest first).
- commentsFor(designerId, limit) — top-level comments only for v1
- toggleLike — flips like state, returns new count + liked boolean
- postComment — name+body validation, 1500-char body cap, 5/hour
burst limit per anon, simple spam regex
(viagra/crypto/loan/casino/etc.), max 2 URLs allowed
Routes (in routes/public.js):
GET /feed — page=N (15 per page)
GET /api/clients/:id/comments — JSON list, top-level comments
POST /api/clients/:id/like — toggle, JSON {liked, like_count}
POST /api/clients/:id/comment — JSON {author_name, author_email?, body}
views/public/feed.ejs — max-width 520px column. Each card:
- Avatar circle (made-with-ai fallback for unclaimed)
- Name (links to /clients/:slug) + firm/role · city,state
- Areas-served chips
- Site/LinkedIn/Instagram link row
- Like / Comment / Share action row
- Collapsed comments section that lazy-loads on toggle
- Inline comment form (name+email+body, optimistic insert on success)
- At <520px width, posts go edge-to-edge (FB-style mobile)
server.js — added cookie-parser middleware.
Smoke-tested:
GET /feed → 200, 15 posts rendered
GET /api/clients/45/comments → 200, JSON list
POST /api/clients/45/like → 200 {liked:true, like_count:1}
POST /api/clients/45/comment → 200, comment landed
v1 scope cuts (defer to next ticks if Steve wants):
- Nested replies (parent_id is in schema but not used yet)
- Infinite scroll (page=N + Older/Newer pagers for now)
- User accounts (anon cookie is enough)
- Notifications + email-on-comment
- Designer claim flow to enable Verified badge on their post
1f43d2b · 2026-05-12 · feat(starsofdesign): regex pass extracts URLs from stored email signatures
scrapers/extract-links-from-signatures.js — cheap free pass over every
sod_email_candidates.last_signature blob. Regex pulls all http(s) URLs,
classifies as:
linkedin linkedin.com/in/* OR linkedin.com/company/*
instagram instagram.com/*
houzz houzz.com/*
pinterest pinterest.com/*
facebook facebook.com/*
twitter twitter.com/* or x.com/*
firm-site any URL whose host matches the candidate's email domain
(so jane@studio.com signature with https://studio.com/about
gets recognized as firm-site)
Otherwise dropped (calendar/mailer/asset hosts are noise without curation).
Idempotent via ON CONFLICT DO NOTHING on sod_links. Designer-attached
when sod_designers.primary_email matches the candidate, otherwise
inserted as orphan (designer_id=NULL, firm_id=NULL) waiting to be
attached when the designer's profile gets materialized in a later
sig-extract pass.
First run results:
scanned 158 signatures
inserted 24 new links
by-kind: firm-site=11 twitter=5 instagram=4 facebook=2 houzz=1 pinterest=1
dupes=3
Why this matters — the LLM in extract-from-signatures captures 1-2 URLs
per signature at most (usually just the primary website or the linkedin),
but real designer signatures often include 4-6 URLs across Instagram /
Houzz / personal site / portfolio host / etc. This deterministic pass
sweeps up the rest for free.
Top-40 firm LinkedIns from seed-top-firms.js confirmed still in place
(40 LinkedIn URLs at firm_id level for Gensler/HOK/Kelly Wearstler/etc.).
Designer-level LinkedIn coverage is the next gap — needs Google CSE
keys to resolve via 'site:linkedin.com/in NAME FIRM' search. Steve does
not have GOOGLE_CSE_KEY / GOOGLE_CSE_ID registered in secrets-manager
yet; tick blocked on Steve providing those.
Background drains running:
PID 59491 — asseeninmovies wikidata bg4 (15k movies, popularity-sorted)
PID 59492 — starsofdesign sig-extract pass 4 (limit=200)
feeab56 · 2026-05-12 · feat(starsofdesign): /clients route — surface real DW client designers in the directory
New tier in the directory:
/designers editorial 150-name JSON marquee (Kelly Wearstler etc.) — unchanged
/clients NEW — materialized real DW client designers from
sig-extract + Gmail recon
/clients/:slug NEW — individual client profile page
Files:
lib/clients.js PG query layer. listClients({q,area}) does the
sod_designers ↔ sod_designer_firm ↔ sod_firms ↔
sod_links join, groups links by kind (linkedin /
firm-site / instagram), rolls up city/state/areas
preferring firm's over designer's. getClient(slug)
fetches one + its links.
routes/public.js Adds GET /clients (list, search by q + area filter)
and GET /clients/:slug (detail). 80-char slug regex
guard. PG queries via pg Pool.
views/public/clients.ejs List page. Same card pattern as /designers but
with area chips + LinkedIn/Website/Instagram
link row. Hero says 'The designers and firms
who actually buy from Designer Wallcoverings.'
views/public/dwclient.ejs Detail page. Headshot (made-with-ai fallback)
+ name + VERIFIED chip (when claimed) + role +
city/state + areas-served tags + Links list +
Claim CTA + Firm section.
Gotcha codified to memory (feedback_express_ejs_client_local_reserved.md):
Passing { client: c } to res.render silently shadows EJS's include()
helper via with(locals). Symptom: 'include is not a function' at line 1
of any <%- include('...') %>. Took 6 restarts + minimal-template
bisection to find. Workaround: use any local name other than 'client'.
Renamed to 'profile' here.
Sig-extract pass 3 (--limit=150, ran in background during this UI work):
ok=74 withName=11 withFirm=14 skipped=76
Real sod_designers: 24 → 31 (+7)
New surfaces include: Putri Sarnadi @ Studio Left, Jesus Raya @ Tokiwa
Tsusho (Long Beach CA, with 2 areas served — the new field working).
Smoke-tested live:
GET /clients → 200, 21 designer cards
GET /clients/jo-hempelmann → 200, Katie's Wallpaper Installation, LA
GET /clients/jerry-sanflippo → 200, Fos Interiors, Palm Springs
fcd8cd8 · 2026-05-12 · feat(starsofdesign): areas_served + PII guardrails + sig-extract pass 2
Per Steve's correction 2026-05-12: 'city and state ok, no zip, no
street address, and AREAS THEY SERVE.'
Schema change (idempotent ALTER):
ALTER TABLE sod_designers ADD COLUMN areas_served TEXT[];
ALTER TABLE sod_firms ADD COLUMN areas_served TEXT[];
CREATE INDEX (gin) on each.
LLM extractor prompt rewrite (extract-from-signatures.js SYSTEM):
- Asks for {full_name, firm_name, firm_city, firm_state, firm_country,
firm_role, areas_served[], linkedin_personal, linkedin_company,
website, instagram}.
- Explicit rule: NEVER extract street address, NEVER extract zip,
NEVER extract phone. Phone is opt-in via claim flow, not scraping.
- For areas_served: look for 'Serving:', 'Areas:', 'We cover', or
parenthetical region phrases. Otherwise safely infer from city+state.
- If a signature contains '123 Main Street, Suite 4B, Brooklyn, NY 11201'
we keep 'Brooklyn'/'NY' only — never the street or zip.
Server-side PII cleanup of LLM output (in case the model echoes one
through anyway):
- firmCity scrubbed of any 'N <street> Street/Ave/etc.' patterns
- firmCity scrubbed of zip patterns (\d{5} and \d{5}-\d{4})
- areas_served filtered: no leading digits, no zip-shaped entries,
no <2-char or >60-char entries, dedupe + cap at 12.
Hallucination hardening (carryover from Steve's earlier note about
'Designer Wallcoverings' false firm):
- Reject bare first-name without local-part match: gemma3:12b
repeatedly returns 'Samantha' / 'Steve' / 'Sarah' when it can't find
a real name. New guard requires the first-name's first 4 chars to
appear in the email local-part.
- Reject firms that are just a website ('domaineinteriors.com').
Two-phase candidate select for the pass:
Priority 1 — empty display_name (need name to materialize designer)
Priority 2 — has name but no sod_designers row yet (firm/role enrichment)
Persisted in this pass:
city / state_or_region / country / areas_served on both sod_firms and
sod_designers. Instagram URL gets its own kind='instagram' row in
sod_links (in addition to existing linkedin / firm-site kinds).
Sig-extract pass 2 results (limit=120):
ok=51 withName=9 withFirm=13 skipped=69
9 new real designers materialized after hardening cleanup:
Lynsey Humphrey · Alder & Tweed
Lara Heffernan · Berg Development · Brookfield
Ashley Kruchoski · Katiebassett · Minneapolis
Jerry Sanflippo · Fos Interiors · Palm Springs
Amy L Young · Eric Hedlund Design · Coeur d'Alene
(+ 4 noise rows wiped pre-hardening)
Memory codified: feedback_designer_directory_pii_policy.md.
Ledger now:
sod_designers (real) 24 (was 15)
sod_firms 104 (was 92)
sod_links 86 (was 83)
candidates_real 562
candidates_with_name 427
Background jobs:
PID 29524 asseeninmovies wikidata bg3 (10k more, 7873/290945 done)
a7bdb25 · 2026-05-12 · feat(starsofdesign): DEEP George info@ recon (7-window × 8-template walk)
scrapers/george-deep-recon.js — date-sliced sweep across info@
designerwallcoverings.com. Seven date windows (last-30d, 30d-90d,
90d-180d, 180d-1y, 1y-2y, 2y-3y, 3y-5y) × eight query templates:
info-replied-to from:info@dw — outgoing reply, designer in To:
info-replied-steve from:steve@dw — same pattern for Steve's DW account
sample subject:sample|memo|swatch|free-sample
trade subject:trade|wholesale|trade-account|program
quote subject:quote|pricing|custom|net-price
project subject:project|residence|hospitality|hotel|...
role-keyword subject:designer|interior|residential|specifier|...
mailer-reply subject:Re:|RE: in:inbox (designer replied to a
DW campaign)
For outgoing-info@ messages (info-replied-to / -steve), pulls the full
message via /api/messages/:id and reads the To: header — that's where
the actual designer lives. For incoming, parses From:.
Every email checked against scrapers/vendor-blocklist.json before
landing as is_designer.
Result of this run (max-per-q=50):
messages_scanned 2306
clients_new_or_bumped 1459
vendors_caught 554
no_email 293
Blocklist additions for vendors that slipped this sweep (28 more
caught + flipped): bmwallpaper, miltonandking, wolfgordon, wolf-gordon,
mindtheg, cowtan, surfaceprint, em.spoonflower, spoonflower, fypon,
quintushome, brewp, lelievreparis, shawcontract, patcraft, interface,
tarkett, milliken, mannington, armstrong, carnegiefabrics,
robertallendesign, lee-jofa, thibautcontract, stoutindustries,
pkaufmann, rmcoco, barrowfabrics, duralee, swavelle, knolltextiles,
carnegiexorel, wallpaperworld, wallpaperboulevard, wallpaperdirect,
rebelwalls, graham-brown, anewall, wallsneedlove.
Also widened extract-from-signatures.js to fall back through accounts:
info → steve-office. Many candidates only have signal in steve@dw
mailbox not info@.
Final ledger:
candidates_total 793 (was 343, +450)
real_designers 585 (was 200, +385 net)
real_designers_with_name 412 (was 77, +335 — direct From: parsing
caught most of them)
real_designers_empty_name 173 (still need sig-extract or manual)
vendors_quarantined 150 (was 86, +64 — blocklist working)
noise 58
Top new domains: gmail.com (75, mostly real interior designers using
personal gmail addresses), yahoo.com (9), hotmail.com (6), aol.com (6),
lbiboyd.com, apbuildersusa.com, dugganla.com, rudindonner.com, etc.
Sig-extract --limit=100 running in background (PID 94284) on the 173
empty-name pool. Wikidata bulk drain 10k batch 2 also kicked for
asseeninmovies (PID 96412).
f5688ed · 2026-05-12 · feat(starsofdesign): signature-block LLM extraction → Jo Hempelmann shipped
scrapers/extract-from-signatures.js — for each candidate with empty
display_name (148 such), pull most-recent George info@ message body,
slice to signature region (after -- delimiter or last 14 non-empty
lines), gemma3:12b in JSON-mode extracts:
full_name, firm_name, firm_city, firm_role,
linkedin_personal, linkedin_company, website, phone.
Then materializes:
- UPDATE sod_email_candidates.display_name + last_signature
- INSERT sod_designers + sod_firms with role/city
- INSERT sod_designer_firm join with extracted title
- INSERT linkedin/website URLs into sod_links (kind='sig-extract')
Two important guardrails:
- Length sanity check on name (3-80 chars) and firm (3-120 chars).
- Hard refusal of 'Designer Wallcoverings*' as firm_name — that's
DW's OWN outgoing signature embedded in the contact's reply, NOT
their firm. LLM grabbed it from rpaschall@pinecrestinsurance.com,
kcfox007@gmail.com, angela@handwerk.company first run; guard
added, hallucinated 'Designer Wallcoverings and Fabrics' firm row
deleted.
First-pass result on 25 highest-signal empty-name candidates:
jo@katieswallpaper.ca → Jo Hempelmann, Katie's Wallpaper Installation
Inc., Los Angeles, Wallpaper Installation Project Manager
export@bmwallpaper.com → Jenny Liu (name only — no firm in sig)
jcamacho427@gmail.com → Jerry Camacho (later flagged is_noise —
Steve's accountant, not a designer)
rpaschall / kcfox007 / angela@handwerk → hallucinated DW (guarded
after run)
17 of 25 had no message in info@ (personal-Gmail-only contacts)
Manual cleanup tied to this tick (27 candidates flipped is_noise):
Family: nataliaabrams (Steve's wife), bobfox729, billfox92117,
jiabramsla, dylan.r.fox, kcfox007 (all relatives).
Service providers / vendors: hrftax53 (accountant), jcamacho427
(accountant), tom@osbornepartners (financial advisor), michael@
montereyrentals (landlord), mike@5to1web (IT vendor), rpaschall@
pinecrestinsurance (insurance), angela@handwerk, email@rentmanager,
circulation1@labj*, jimdultz@comcast (vendor),
carole.fabrics@carolefabrics, helen.brier.b6@mail.toray (Toray),
kundservice@photowall (Photowall), noreply@zopim (transactional),
natalia/sabrina@studentdebtcrisis (wife's nonprofit), chounanian
(ex-DW VP), browntwn / mitz / greg_perry (personal).
Ledger after this tick:
real_designer_candidates 200 (was 225 — 25 dropped to noise)
noise_total 58 (was 31)
vendors_total 84
sod_designers (real) 4 (was 1 — Jo Hempelmann added)
sod_designers (reps) 40
sod_firms_total 82 (+1 Katie's Wallpaper, hallucinated DW removed)
41d434d · 2026-05-12 · feat(starsofdesign): pull DW client contacts from George info@ Gmail
scrapers/george-info-recon.js — walks 6 client-flavored Gmail searches
on info@designerwallcoverings.com via the now-portable George agent
(commit a4d0448 fixed the Mac/Kamatera path issue). Each search target
is a different DW-client signal:
shopify-order from Shopify order-notification mailers; extracts
customer billing email FROM THE BODY (not the
From: shopifyemail.com header). signal=0.97
trade-signup subject:trade|wholesale. signal=0.93
sample-request subject:sample|memo|swatch. signal=0.85
quote-request subject:quote|custom|pricing. signal=0.82
project-inquiry subject:project|spec|residence|hosp. signal=0.78
designer-subject subject:interior|residential|hosp. signal=0.78
Every email is checked against scrapers/vendor-blocklist.json before
landing as is_designer — vendors get logged at status='is_vendor' (audit
trail) with score=0 so they can't be promoted later.
Tick results — 480 messages scanned across 6 queries:
clients (new is_designer) 212
vendors filtered 36
self (info@dw) 151
no extractable email 81
Plus 3 new vendor domains caught + added to blocklist + flipped:
koroseal.com, innovationsusa.com, kneedlerfauchere.com,
oalusa.com, archiproducts.com, elitis.us, ccsend.com,
billtrust.com, battagliaincmarble.ccsend.com → flipped 16 candidates
to is_vendor.
Final ledger after George recon + cleanup:
candidates_total 343 (was 236, +107 net new clients)
real_designer_candidates 225
vendors_filtered 86
noise 31
sod_firms 81 (top-40 seed + 40 reps + 1)
Real DW-client surfaces newly captured (top by domain):
dugganla.com (Duggan Interiors LA)
apbuildersusa.com (AP Builders)
rudindonner.com (Rudin & Donner NYC)
dkmdesignstudio.com (DKM Design Studio)
egg-and-dart.com (San Diego interiors)
davidsonlondon.com (Davidson London)
+ 64 more.
Next tick: signature-block parser to extract firm/role/city from each
candidate's most-recent message body, then promote to sod_designers.
4e7adb5 · 2026-05-12 · fix(starsofdesign): vendors are NOT clients — apply blocklist, seed top-40 US firms
Steve correction 2026-05-12: paulmontgomery, desima, newwall, beverlyhillswallpaper are VENDORS (manufacturers DW buys from), NOT designer clients. And beverlyhillswallpaper.com is dead — never use. Three changes: scrapers/vendor-blocklist.json — codified the rule. Every domain with a ~/.claude/skills/*-scraper-manager/ skill is by definition a vendor we buy from. Plus the wider blocklist from Steve's prior corrections (Kravet, Schumacher, Scalamandre, Fabricut, Materialbank, Donghia, Stroheim, Maharam, Pollack, Phillip Jeffries, Cole&Son, Romo, Osborne& Little, Thibaut, Pierre Frey, Mind the Gap, etc.). scrapers/apply-vendor-blocklist.js — bulk-flips existing candidates that got auto-promoted to status='is_designer' in tick 1+2. Removes any sod_designers/sod_firms rows whose primary_email is on the blocklist. Idempotent. Result: 43 candidates → is_vendor, 1 → is_noise (beverlyhillswallpaper), 3 wrongly-created sod_designers rows deleted. data/top-us-firms.json + scrapers/seed-top-firms.js — deterministic seed of 40 top US architecture (Gensler / HOK / Perkins&Will / SOM / KPF / NBBJ / HKS / Cannon / HDR / Stantec / Corgan / Page / Gresham Smith / Cooper Carry / Leo A Daly / Pickard Chilton / DS+R / Snohetta / CallisonRTKL / AECOM) + 20 top US interior design (Kelly Wearstler / Studio Sofield / Nate Berkus / Studio McGee / Shamshiri / Magnolia / Champalimaud / Yabu Pushelberg / Roman&Williams / Rockwell / Bunny Williams / Victoria Hagan / Markham Roberts / Steven Gambrel / Robert Stilin / Ken Fulk / Suzanne Kasler / Bilhuber / Miles Redd / Sheila Bridges) firms. Inserts into sod_firms with public LinkedIn company slugs + firm-website URLs in sod_links. Logos stay made-with-ai placeholders until each firm claims its profile. Ledger after correction: sod_firms_total 81 (+40 from seed) sod_firms_50plus 18 sod_links_linkedin 40 sod_links_firmsite 40 candidates_is_vendor 48 (was 5) candidates_is_designer 156 (was 200 — vendors flipped out) candidates_total 236 Memory codified: feedback_starsofdesign_clients_not_vendors.md. BLOCKER surfaced for Steve: George Gmail agent at 127.0.0.1:9850 has hasCredentials=false. Need OAuth consent flow run so George can read DW info@ inbox. That inbox (Shopify order notifications, trade signups, sample requests) is where DW's actual designer CLIENTS live — much higher signal than Steve's personal Gmail (which is heavy on vendor sales reps emailing him for buy-side relationships).
fed7183 · 2026-05-12 · feat(starsofdesign/tick2): local-LLM triage on remaining candidates
scrapers/triage-candidates.js — drives Ollama (gemma3:12b by default)
in JSON-mode to label each sod_email_candidates row as one of:
is_designer / is_firm / is_vendor / is_press / is_noise. Writes both
the status enum and a human-readable triage note (label, confidence,
short reason) into notes via CONCAT_WS so the original signal-score
note is preserved.
Why gemma3:12b not qwen3:14b — first pass at qwen3:14b returned empty
content for all 39 rows because its <think> mode swallows the entire
num_predict=200 budget before producing JSON. Switched default to
gemma3:12b which is ~0.5s/row and JSON-stable.
Deterministic short-circuit for masked customer rows — the 25 digital_
sample_orders rows wear synthetic customer{1..25}@example.com addresses
(privacy-mask in dw_unified.digital_sample_orders); those skip the LLM
entirely and resolve straight to is_noise.
Tick 2 results on 39 still-new candidates:
is_designer 2 (paulmontgomery.com, beverlyhillswallpaper.com)
is_firm 2 (desima.com, newwall.com)
is_vendor 5 (priti, printmurals, fahertybrand, toplinegraphics, profectionpainting)
is_press 0
is_noise 30 (25 masked-customer + studentdebtcrisis/filmla/trueaccord/etc.)
errors 0
Status snapshot post-tick:
candidates_total 236 (was 236)
status_new 0 (was 39 — fully drained)
status_is_designer 200
status_is_firm 1 (4 firm-labeled rows kept their LLM label after
promote-candidates.js attempted name extraction)
status_is_vendor 5
status_is_noise 30
Known gap (for tick 3): promote-candidates.js can't materialize sod_designers
/sod_firms rows when display_name is empty — guessNameAndFirm() returns
{person:null, firm:null} and produces designer=null firm=null. Tick 3's
signature-block parser fixes this by extracting names from each
candidate's last-seen email signature. Tick 2 deliberately doesn't try to
backfill name-from-domain because that's tick 3's job and the cleaner
fix.
1542658 · 2026-05-12 · feat(starsofdesign/tick1): Gmail + Shopify recon → 236 candidates / 44 designers / 41 firms
Two-source ingest into sod_email_candidates per Steve's spec. Gmail side (scrapers/gmail-recon-ingest.js): - Sub-agent walked steveabramsdesigns@gmail.com via claude.ai Gmail MCP, scoping queries to 'from:me newer_than:5y' + 8 design-flavored variants going back to 2017. 279 reply threads scanned, 203 unique non-Steve participants extracted to /tmp/sod-candidates.ndjson. - Drop-list filters noreply/notifications/SaaS-vendor noise (~25 domains). - Receiver script computes signal_score = reply_ratio*0.85 + domain_bonus (design/studio/interiors/architect = +0.20). - 190/203 land at signal >= 0.8. Shopify side (scrapers/shopify-recon.js) — added per Steve 'also use shoppy database': - dw_unified.trade_signups (7 rows): verified DW trade-program signups. Land at signal=0.95, status='is_designer' (auto-promoted, skip triage). - dw_unified.digital_sample_orders (25 unique): paid sample customers. Land at signal=0.70, status='new' for later triage. - designer_representatives.designer_reps (40 rows): manufacturer rep agencies. Direct-insert into sod_designers + sod_firms (skip candidate stage entirely — they're already curated) with role='Manufacturer Rep' and a 'made-with-ai' headshot. Auto-promotion (existing scrapers/promote-candidates.js, --min=0.85): - 197/236 candidates flipped to status='is_designer' or 'is_firm'. - Most Gmail rows have empty display_name (MCP returns bare emails for many threads); follow-up tick will do get_thread name extraction. Idempotent: all INSERTs use ON CONFLICT with GREATEST(signal_score) and LEAST/GREATEST first_seen/last_seen so re-runs only enrich. Ledger after this tick: sod_email_candidates 236 (197 promoted, 39 awaiting triage) sod_designers 44 sod_firms 41 sod_designer_firm 41 sod_links 0 (designer_reps have no URLs/emails on file)
70b7912 · 2026-05-10 · add /sitemap.xml route + JSON-LD Person schema on designer detail
- robots.txt already advertised /sitemap.xml; route was missing (404). Now serves all 4 static pages + every designer slug with lastmod/priority. - Detail pages now emit schema.org Person JSON-LD (name, bio, styles via knowsAbout, city via workLocation, DW pairings as subjectOf) so SERP cards get rich-result eligibility.
File tree
- .deploy.conf
- .gitignore
- Makefile
- README.md
- data/designers.json
- data/schema-pg.sql
- data/top-us-firms.json
- data/vendor-domains.txt
- data/videos.json
- docs/PLAN.md
- ecosystem.config.js
- lib/clients.js
- lib/data.js
- lib/feed.js
- lib/firms.js
- lib/sort.js
- package-lock.json
- package.json
- public/css/site.css
- public/css/theme.css
- public/favicon.ico
- public/favicon.svg
- public/img/made-with-ai.svg
- public/js/grid-controls.js
- public/js/hamburger.js
- public/js/theme-toggle.js
- public/robots.txt
- routes/public.js
- scrapers/apply-vendor-blocklist.js
- scrapers/extract-from-signatures.js
- scrapers/extract-links-from-signatures.js
- scrapers/george-deep-recon.js
- scrapers/george-info-recon.js
- scrapers/gmail-recon-ingest.js
- scrapers/promote-candidates.js
- scrapers/seed-top-firms.js
- scrapers/shopify-recon.js
- scrapers/triage-candidates.js
- scrapers/vendor-blocklist.json
- server.js
- test/smoke.js
- views/partials/footer.ejs
- views/partials/grid-controls.ejs
- views/partials/head.ejs
- views/partials/header.ejs
- views/public/404.ejs
- views/public/about.ejs
- views/public/clients.ejs
- views/public/designer.ejs
- views/public/designers.ejs
- views/public/dwclient.ejs
- views/public/error.ejs
- views/public/feed.ejs
- views/public/firm.ejs
- views/public/firms.ejs
- views/public/home.ejs
- views/public/privacy.ejs
- views/public/submit.ejs
- views/public/terms.ejs
- views/public/videos.ejs