Purpose
Collect visible comments from recent posts by a LinkedIn member identified by {person-name} and from posts published by a company page identified by {company-name}. Optionally verify whether a company page contains a visible post matching {post-marker} or {post-text}. Return post and comment metadata so the caller can determine prospect qualification. This is read-only and requires an authenticated session; the company-admin surface additionally requires page-admin authorization.
When to Use
Use when the caller wants to review comments on a person's recent LinkedIn posts and/or posts published by a company page, or wants to verify that a company page published a post matching supplied text or a distinctive marker. Use the caller's own prospect criteria after extraction; do not infer prospect status solely from a name, title, or comment.
Workflow
- Require an approved authenticated LinkedIn browser session. Do not enter or store credentials. Stop and report an access failure for a login wall, CAPTCHA, checkpoint, consent gate, or unauthorized company-admin page. If
/feed/redirects to LinkedIn login, skip the feed and navigate directly to the authorized company-admin URL instead; do not treat the redirect as evidence that the company has no posts. - Resolve the member's opaque profile URL directly through:
https://www.linkedin.com/search/results/people/?keywords={encodeURIComponent(person-name)}On the loaded page, choose an exact visible-name match when available and read its canonical/in/{slug}/href. Do not guess the slug. Navigate directly to:https://www.linkedin.com/in/{resolved-profile-slug}/recent-activity/all/ - Resolve the company identity through:
https://www.linkedin.com/search/results/companies/?keywords={encodeURIComponent(company-name)}Select the exact or strongest unambiguous result and read its canonical company href or numeric company identifier. Never fabricate an identifier. If the caller supplies a canonical company-admin URL or numeric ID, use it directly. For an authorized page-admin session, navigate directly to:https://www.linkedin.com/company/{company-id}/admin/page-posts/published/If only a public company page is authorized, use its supplied or resolved slug athttps://www.linkedin.com/company/{company-slug}/posts/instead and report if that surface is unavailable. - On each loaded recent-activity or company-posts page, wait for dynamic cards to settle, then run this evaluator. It extracts visible posts and nested comments from the current page only; run it once per page and union results by activity URN and comment key:
(() => {
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
const abs = href => { try { const u = new URL(href, location.href); u.search = ''; u.hash = ''; return u.href; } catch { return null; } };
const postNodes = [...document.querySelectorAll('div.feed-shared-update-v2, article[data-urn*="activity"], [data-urn*="activity"]')]
.filter((el, i, a) => !a.some((other, j) => j !== i && other.contains(el)));
const seenPosts = new Set(), posts = [];
for (const post of postNodes) {
const raw = post.getAttribute('data-urn') || post.getAttribute('data-id') || post.querySelector('[data-urn*="activity"]')?.getAttribute('data-urn') || '';
const activityUrn = (raw.match(/urn:li:activity:\d+/) || [])[0] || null;
if (!activityUrn || seenPosts.has(activityUrn)) continue;
seenPosts.add(activityUrn);
const postLink = post.querySelector('a[href*="/feed/update/"], a[href*="/posts/"]');
const time = post.querySelector('time');
const comments = [], seenComments = new Set();
for (const c of post.querySelectorAll('.comments-comment-item, article.comments-comment-item, [data-test-id="comment-item"], .comments-comment-item-content')) {
const cRaw = c.getAttribute('data-urn') || c.getAttribute('data-id') || '';
const commentUrn = (cRaw.match(/urn:li:comment:\d+/) || [])[0] || null;
const authorEl = c.querySelector('.comments-post-meta__name-text, .comments-comment-item__actor-name, [data-anonymize="person-name"], a[href*="/in/"]');
const author = clean(authorEl?.innerText || authorEl?.textContent);
const authorLink = authorEl?.closest('a') || c.querySelector('a[href*="/in/"]');
const content = c.querySelector('.comments-comment-item__main-content, .comments-comment-item-content, [data-test-id="comment-text"]');
const text = clean(content?.innerText || c.innerText);
const key = commentUrn || `${author}|${text}`;
if (!text || seenComments.has(key)) continue;
seenComments.add(key);
const ct = c.querySelector('time');
comments.push({ commentUrn, author: author || null, authorUrl: authorLink ? abs(authorLink.href) : null, postedAt: ct?.getAttribute('datetime') || clean(ct?.innerText) || null, text });
}
posts.push({ activityUrn, url: postLink ? abs(postLink.href) : null, author: clean(post.querySelector('.update-components-actor__name, .feed-shared-actor__name')?.innerText) || null, postedAt: time?.getAttribute('datetime') || clean(time?.innerText) || null, text: clean(post.querySelector('.update-components-text, .feed-shared-update-v2__description, .feed-shared-text')?.innerText) || null, comments });
}
return { pageUrl: location.href.split('?')[0], posts };
})()- For a company-post verification request, after loading the authorized published-posts page, use this evaluator on the current page. Supply
{post-marker}for a distinctive phrase such as a campaign name, or{post-text}for a longer expected text fragment:
(({ marker, text }) => {
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
const wanted = clean(marker || text).toLowerCase();
const posts = [...document.querySelectorAll('div.feed-shared-update-v2, article[data-urn*="activity"], [data-urn*="activity"]')]
.filter((el, i, a) => !a.some((other, j) => j !== i && other.contains(el)))
.map(post => {
const raw = post.getAttribute('data-urn') || post.getAttribute('data-id') || post.querySelector('[data-urn*="activity"]')?.getAttribute('data-urn') || '';
const activityUrn = (raw.match(/urn:li:activity:\d+/) || [])[0] || null;
const link = post.querySelector('a[href*="/feed/update/"], a[href*="/posts/"]');
const body = clean(post.querySelector('.update-components-text, .feed-shared-update-v2__description, .feed-shared-text')?.innerText || post.innerText);
const time = post.querySelector('time');
return { activityUrn, url: link ? new URL(link.href, location.href).href.split('?')[0].split('#')[0] : null, postedAt: time?.getAttribute('datetime') || clean(time?.innerText) || null, text: body };
}).filter(p => p.activityUrn && p.text);
const matches = wanted ? posts.filter(p => p.text.toLowerCase().includes(wanted)) : posts;
return { success: true, pageUrl: location.href.split('?')[0], checkedPosts: posts.length, matched: matches.length > 0, matches, note: wanted ? 'Matching is case-insensitive substring matching against visible post text.' : 'No marker supplied; returned visible posts only.' };
})({ marker: '{post-marker}', text: '{post-text}' })Return matched: false only when the page is accessible and the requested visible text is absent from the currently loaded posts. If more posts are required, use the page's visible load-more control or bounded scrolling, wait for newly rendered cards, rerun the evaluator, and deduplicate by activityUrn.
6. Return the two source collections separately, preserving document order. Mark prospect qualification as caller-side analysis.
Site-Specific Gotchas
- Member activity uses the durable path
/in/{profile-slug}/recent-activity/all/; resolve the opaque slug from people search when only a name is supplied. - The authorized company-admin publishing surface is
/company/{numeric-company-id}/admin/page-posts/published/. Numeric company IDs are opaque and must be read from a resolution result, a supplied canonical URL, or the current page URL; never guess them. - The company-admin route may redirect or deny access for non-admin sessions. Treat that as an authorization failure, not an empty post list; use the public
/company/{slug}/posts/surface only when appropriate and accessible. - A navigation to
/feed/may redirect to/uas/login?session_redirect=...; this is an authentication signal. Navigate directly to the authorized admin publishing route in an approved session. - LinkedIn content is dynamically rendered. Use
domcontentloadedfollowed by a short settling wait before evaluation. - Comments may be collapsed or partially loaded. Extract only comments present in the current DOM; bounded load-more or scrolling may be needed for a fuller collection.
- Activity and comment cards can expose identifiers in
data-urn,data-id, or nested activity-bearing elements. Deduplicate because the same card may appear through multiple matching selectors. - A login wall, CAPTCHA, checkpoint, consent page, or anti-automation challenge is an access failure, not evidence of no comments or no matching post.
- Text may be truncated behind
MoreorSee more; expand only those visible controls before extraction when full text is needed. - The verification evaluator matches only visible text on the currently loaded page. It does not prove that a post never existed if older posts are not loaded or LinkedIn restricts the feed.
- Keep the workflow read-only and same-origin. Do not collect credentials, cookies, tokens, private messages, or unrelated profile data.
Expected Output
{
"success": true,
"person": {"name": "{person-name}", "profileUrl": "https://www.linkedin.com/in/{resolved-profile-slug}/"},
"company": {"name": "{company-name}", "pageUrl": "https://www.linkedin.com/company/{company-id}/admin/page-posts/published/"},
"personPosts": [{"activityUrn": "urn:li:activity:{id}", "url": "...", "author": "...", "postedAt": "...", "text": "...", "comments": [{"commentUrn": "urn:li:comment:{id}", "author": "...", "authorUrl": "...", "postedAt": "...", "text": "..."}]}],
"companyPosts": [],
"verification": {"matched": true, "matches": [], "checkedPosts": 0},
"accessErrors": []
}For verification-only requests, the relevant result is {success, pageUrl, checkedPosts, matched, matches}. If the page is blocked or unauthorized, return an explicit access failure rather than an empty or negative verification result.