Skip to content

feat(leads): find the people a directory lists, not just the sites it links to - #144

Merged
ralyodio merged 1 commit into
masterfrom
feat/person-discovery
Jul 28, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/person-discovery

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Pointed at ctodirectory.com, discovery returned one host — stackup.group, the directory's own footer — from a page naming sixteen people. Link-following assumes a listing points at each business's own site; a people directory publishes names and links only to itself.

People are read as entities

Structured data first. Every JSON-LD node is checked, not the first — a profile page's first block is normally the site's own Organization, which is the difference between a named CTO and the directory's parent company.

Collected alongside prospects, never instead of them. A directory page often offers both: the people it lists and its own info@.

Two loading bugs, same wrong question

loadSeedHtml decides whether to render by asking "did a plain fetch produce any outbound candidate" — and a directory's own footer answers yes. So the shell was accepted, the client-rendered listing never loaded, and the profiles were invisible. It now also asks whether the page yielded entries, and an entity page that names nobody is retried with a render.

The meta fallback nearly shipped fabricated people

First live run produced four "people":

AI Leadership Sprint · For Fractional CTOs · For CEOs and Founders · For Board Members

All capitalised, all 2–4 words, none human. A fabricated name reaches a real inbox addressed to nobody. A title is now only read as a person when the page itself claims to be a profile (/profile/… or og:type=profile), and headline words are refused. All four are pinned as tests.

LinkedIn URLs normalised

Members paste the URL from their own logged-in view — linkedin.com/public-profile/settings/?trk=…, a settings page that opens nothing for anyone else. Looks like a working link until clicked. Now rejected; only /in/, /company/, /school/ survive, tracking params stripped.

Verified live

12 people | 12/12 via json-ld | 12 with title | 11 with LinkedIn
Marc van Neerven | Chief Technology Officer | linkedin.com/in/mvneerven
Eoin Woods       | Owner and CTO | Artechra  | linkedin.com/in/eoinwoods

Employers embedded in a title ("Owner and CTO at Artechra") are split out, since directories fill jobTitle and leave worksFor empty.

Limits

One-shot lead finding was capped at 10 — a directory lists hundreds, so it looked broken. Raised to 1000, with an explicit ceiling on contact lookups so a large run can't become an unbounded bill.

Checks

  • tsc --noEmit clean
  • 890/890 tests pass, 27 new
  • production build compiles

🤖 Generated with Claude Code

… links to

Link-following assumes a listing points at each business's own site. A
people directory does not: it publishes names, titles and locations, and
links only to itself. Pointed at one, discovery returned a single host —
the directory's own footer — from a page naming sixteen people.

So a person is now read as an entity. Structured data first, because a
page carrying schema.org Person markup has already said exactly who it is
about and nothing needs inferring from prose. Every JSON-LD node is
checked rather than the first: a profile page's first block is normally
the site's own Organization, which is the difference between a named CTO
and the directory's parent company.

People are collected alongside prospects, never instead of them. A
directory page frequently offers both — the people it lists and its own
contact address — and choosing one discards half of what the page gave.

Two loading bugs had to be fixed before any of it worked, both from the
same wrong question. loadSeedHtml decides whether to render by asking
whether a plain fetch produced any outbound candidate, and a directory's
own footer answers yes — so the shell was accepted, the client-rendered
listing never loaded, and the profiles were invisible. It now also asks
whether the page yielded entries, and an entity page that names nobody is
retried with a render.

The meta fallback is deliberately hard to satisfy, because it produced
four fabricated people on the first live run: "AI Leadership Sprint",
"For Fractional CTOs", "For CEOs and Founders", "For Board Members" — all
capitalised, all two to four words, none of them human. A fabricated name
reaches a real inbox addressed to nobody, so a title is only read as a
person when the page itself claims to be a profile, and headline words
are refused outright. All four are pinned as tests.

LinkedIn URLs are normalised rather than stored as found. Directories
copy whatever the member pasted, and members paste the URL from their own
logged-in view — a settings page with session tracking that opens nothing
for anyone else. That looks like a working link until someone clicks it.

Employers embedded in a title ("Owner and CTO at Artechra") are split out,
since directories routinely fill jobTitle and leave worksFor empty.

Verified against a live directory: twelve people, all from structured
data, twelve with titles, eleven with a usable LinkedIn profile, and the
one pasted settings URL correctly dropped.

One-shot lead finding was capped at ten. A directory can list hundreds,
so reporting ten looked like the form was broken. Raised, with an explicit
ceiling on contact lookups so a large run cannot become an unbounded bill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 882d0a4 into master Jul 28, 2026
8 checks passed
@ralyodio
ralyodio deleted the feat/person-discovery branch July 28, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant