← ALL FIELD NOTES

How to Extract Organization Names and Roles from Staff Pages

A schema-first workflow for extracting staff names, titles, organizations, profile links, and evidence from inconsistent team pages.

Staff-page extraction is a record-association problem

To extract people and roles from websites, it is not enough to find strings that look like names and titles. The hard part is proving that each title, department, image, and profile link belongs to the right person—and, in turn, to the right organization.

A team page may be a clean grid of employee cards, a collection of JavaScript-rendered components, or a long editorial page with headings, biographies, and social links. CSS class names may be opaque and frequently redesigned. A single page can include executives, advisors, alumni, open roles, and quoted customer names. A flat “find all headings, then find all paragraphs” strategy will eventually attach the wrong title to the wrong person.

A more reliable design is:

  1. Define a staff-record schema before writing selectors.
  2. Inspect structured data before interpreting visible markup.
  3. Treat each staff card or profile container as a record boundary.
  4. Use semantic, relative selectors inside that boundary.
  5. Render the page only when the initial document is incomplete.
  6. Store evidence, raw values, and field-level confidence with every result.

This approach is useful for directory products, recruiting research, CRM enrichment, and account research because it makes results explainable when a source site changes.

Start with a schema, not selectors

Your output contract should be stable even when source pages are not. A practical staff record can look like this:

type StaffRecord = {
  name: string | null;
  jobTitle: string | null;
  department: string | null;
  organization: string | null;
  profileUrl: string | null;
  imageUrl: string | null;
  sameAs: string[];

  sourceUrl: string;
  extractedAt: string;
  extractionMethod: "json-ld" | "microdata" | "dom" | "rendered-dom";
  evidence: {
    rawName?: string;
    rawTitle?: string;
    rawOrganization?: string;
    selectorOrPath?: string;
  };
  confidence: {
    name: number;
    jobTitle: number;
    organization: number;
    profileUrl: number;
  };
};

This is closely aligned with the person properties modeled by Schema.org’s Person type, including name, jobTitle, image, url, sameAs, and worksFor. The related worksFor property represents the organization employing the person.

Two implementation choices matter here:

  • Keep raw and normalized values separate. Preserve VP, GTM exactly as found while also storing a normalized title such as Vice President, Go-to-Market if your application needs it. Normalization is useful for search and grouping; raw text is necessary for audits and parser debugging.
  • Score fields independently. A name can be highly reliable while a department is only inferred from a section heading. A page-level success: true hides this important difference.

For example, a record with a linked profile, a visible name, and a title in the same card might receive high confidence for those three fields. If the organization was derived from the site’s header or page metadata rather than the card itself, give that field a lower score and record the source of the inference.

Layer 1: parse JSON-LD and Microdata first

Before selecting visible elements, inspect structured data in the server response. Employee profiles may publish Person entities in JSON-LD or Microdata. Google documents JSON-LD, Microdata, and RDFa as structured-data formats and shows profile-page examples containing person attributes such as names, images, identifiers, and external identity links. See its ProfilePage structured-data documentation.

A JSON-LD pass generally follows this pattern:

function personCandidates(graph: unknown[]): unknown[] {
  return graph.flatMap(node => {
    if (Array.isArray(node)) return personCandidates(node);
    if (!node || typeof node !== "object") return [];

    const value = node as Record<string, unknown>;
    const types = Array.isArray(value["@type"])
      ? value["@type"]
      : [value["@type"]];

    const current = types.includes("Person") ? [value] : [];
    const nested = Array.isArray(value["@graph"])
      ? personCandidates(value["@graph"])
      : [];

    return [...current, ...nested];
  });
}

Then map each candidate into the output schema. Resolve relative URLs against the fetched page URL, accept either a single value or an array for sameAs, and handle worksFor when it is represented as either a string or an organization object.

Structured data is a strong signal, not unquestionable truth. Google’s structured-data policies say markup should represent the main content users can see and warn against markup that does not match visible content. Validate key fields against the rendered or static page when possible:

  • Is the person’s name visibly present?
  • Does the displayed role match jobTitle?
  • Does the profile URL resolve to the employee profile rather than a generic team page?
  • Does the organization in worksFor make sense in the context of the page?

If JSON-LD lists a person but visible content says that person is a former employee, do not silently treat the result as a current staff record. Capture the discrepancy in evidence or apply a source-specific status rule.

Layer 2: find the record boundary before fields

When structured data is absent or incomplete, the staff card is the central parsing unit. Identify a repeated container that represents one individual, then query only within that container.

Consider this simplified markup:

<section aria-label="Leadership">
  <article>
    <a href="/team/maya-chen">
      <img alt="Maya Chen" src="/images/maya.jpg" />
      <h3>Maya Chen</h3>
    </a>
    <p>Chief Technology Officer</p>
  </article>

  <article>
    <a href="/team/daniel-ortiz">
      <img alt="Daniel Ortiz" src="/images/daniel.jpg" />
      <h3>Daniel Ortiz</h3>
    </a>
    <p>VP, Engineering</p>
  </article>
</section>

The desired unit is article, not all h3 elements on the page and not all p elements on the page. Within each card, extract a name candidate, title candidate, image, and primary profile link. Associate the section heading, Leadership, only as context—not as an individual title.

This structure prevents a common failure mode: arrays extracted independently lose their relationship after filtering. If one card lacks a title, names[3] may no longer correspond to titles[3].

Playwright’s locator guidance supports this container-first style. It documents chaining and filtering locators to narrow a search to a particular list item or container, as well as methods for collecting matched text. It also recommends user-facing selectors—roles, text, labels, and alt text—over brittle implementation-specific paths. Read the locator documentation.

A browser-oriented version might use a strategy sequence such as:

const cards = page.locator("article, li, [role=listitem]");

for (const card of await cards.all()) {
  const name = await card
    .getByRole("heading")
    .first()
    .textContent()
    .catch(() => null);

  const profileUrl = await card
    .getByRole("link")
    .first()
    .getAttribute("href")
    .catch(() => null);

  const imageUrl = await card
    .getByRole("img")
    .first()
    .getAttribute("src")
    .catch(() => null);
}

This is intentionally incomplete: not every article on a page is a staff card, and not every staff name is a heading. In production, score candidate containers rather than trusting one universal selector. Signals can include a plausible person name, a nearby role-like string, an image with person-like alt text, and a link whose path resembles a profile route.

Prefer a selector ladder over a single selector

For each field, use ordered alternatives. For a name:

  1. Structured Person.name.
  2. A heading within the card.
  3. The accessible name of the profile link.
  4. Image alt text, when it appears to be a person’s name.
  5. A page-specific fallback selector.

For a title:

  1. Structured jobTitle.
  2. An element with a semantic or explicit title label.
  3. A short text element adjacent to the name in the same card.
  4. A profile-page heading or metadata fallback.

Avoid deep CSS or XPath chains like .grid > div:nth-child(4) > div > span:nth-child(2). The Playwright guidance specifically notes that selectors coupled to DOM structure are prone to break when implementation details change. Semantic locators are not magic, but they tend to reflect the page’s meaning rather than its styling. Playwright’s locator guide explains the tradeoff.

Build extraction outputs your users can verify. Keep the source URL, method, raw text, and confidence with every person record—not just a final name/title pair. Explore PagePith when you need an evidence-oriented workflow for web content.

Layer 3: render only when the initial document is insufficient

Some pages return the team list in the initial HTML. Others insert records after JavaScript runs, after scrolling, or after an API request completes. Rendering every URL is unnecessarily costly and complicates operations, so make it a fallback:

  1. Fetch the document.
  2. Parse structured data and static DOM.
  3. Decide whether the record count and required-field coverage are sufficient.
  4. Render only when evidence indicates missing or incomplete client-side content.
  5. Re-run the same extraction contract against the rendered DOM.

Timing is part of correctness. Playwright locators automatically wait and re-query the current DOM, but its API documentation cautions that locator.all() does not wait for a dynamic list to settle. Waiting for a stable, meaningful condition—such as a staff-list container being visible and no longer changing—is safer than immediately enumerating cards. See the Locator API documentation.

Do not use arbitrary long sleeps as your primary synchronization mechanism. Instead, define a page-specific readiness condition when possible:

  • the staff-list region exists;
  • a loading indicator is gone;
  • card count has remained unchanged across a short polling interval;
  • expected headings or profile links are present.

If a page requires interaction to reveal staff, record that fact in extractionMethod or evidence. A result obtained after expanding “Show more” is materially different from a record found in initial HTML.

Resolve organization carefully

The organization can come from several places, with different reliability:

  1. worksFor on a structured Person entity.
  2. An organization entity connected to the person in the same structured-data graph.
  3. The page’s own visible organization name or site identity.
  4. A section-level label when the page clearly describes a business unit.

Do not infer that every person mentioned on example.com works for the organization that owns example.com. Advisory boards, partners, press quotes, and event speakers often appear alongside staff.

A useful rule is to distinguish stated from inferred organization values. worksFor.name = "Northstar Labs" is stated. “The team page appears under Northstar Labs’ domain” is inferred. Store both provenance and confidence so downstream consumers can decide whether inferred data is acceptable.

Profile URLs and sameAs links are especially valuable for deduplication. Schema.org describes url as the item’s URL and sameAs as a URL that unambiguously identifies the same entity on another reference page. Those identifiers are usually stronger entity-resolution inputs than a name alone, particularly for common names. See Schema.org Person.

Measure quality one field at a time

A parser that returns ten people is not necessarily successful. Track quality by field and by extraction method:

MetricWhat it reveals
Record countMissing pagination, lazy-loaded cards, or false-positive containers
Name coverageWhether a person can be identified
Title coverageWhether role extraction works separately from name extraction
Name-title association accuracyWhether fields are attached to the correct record
Profile-link coverageWhether records can be revisited and deduplicated
Organization provenanceWhether the employer was explicit or inferred
Method distributionHow often JSON-LD, static DOM, or rendered DOM was needed

Create a small regression corpus of real page shapes: card grids, department sections, leadership pages, profile pages, accordions, and JavaScript-loaded directories. Save expected records and test association, not just string presence. When a page redesign breaks a selector, the evidence field should identify exactly which strategy failed.

Respect crawler directives and operational boundaries

Before requesting staff pages at scale, inspect crawler directives and apply your organization’s access, rate, and usage policies. RFC 9309 defines the Robots Exclusion Protocol and makes an important distinction: robots.txt communicates crawler rules, but it is not an authorization mechanism. In other words, honoring robots directives is important crawler behavior, yet it does not itself grant access to content or settle whether a particular use is permitted.

Build controls into the pipeline: request throttling, retry limits, clear user-agent behavior where appropriate, and a way to stop or exclude domains. These controls protect both source sites and the reliability of your own jobs.

An honest PagePith demonstration

The supplied PagePith proof shows a fetch-tier request for https://schema.org/Person. It returned the title “Person - Schema.org Type”, reported a content length of 284,812, and provided a Markdown excerpt beginning with Schema.org’s development-version notice, the canonical URL, and the beginning of a property table.

That is useful evidence that PagePith retrieved this source page as content suitable for inspection. It does not demonstrate staff-card detection, JSON-LD parsing, JavaScript rendering, role association, confidence scoring, or extraction accuracy. Those capabilities should be evaluated against the specific pages and extraction rules your workflow needs.

Make every extracted person auditable

Reliable staff extraction is a layered process: structured data first, a card-level DOM parser second, rendering when necessary, and evidence throughout. The key design decision is not a clever selector. It is preserving the relationship between each person and every field you return.

When you retain source URLs, raw values, extraction methods, and field-level confidence, downstream users can review ambiguous records instead of treating every result as equally certain. That turns a fragile team-page scraper into a maintainable data pipeline.

Sign up for PagePith to evaluate its content retrieval workflow against your own staff-page extraction process.

Sources

  1. Person - Schema.org TypeSchema.org
  2. worksFor - Schema.org PropertySchema.org
  3. ProfilePage Structured DataGoogle Search Central
  4. General Structured Data GuidelinesGoogle Search Central
  5. LocatorsPlaywright
  6. Locator APIPlaywright
  7. Robots Exclusion Protocol, RFC 9309Internet Engineering Task Force
Extract People and Roles from Staff Pages · PagePith