Fetch ONE LinkedIn post with its full context (post + visible comments) from its urn (urn:li:activity:NNN) or its permalink url.
{"urn":"string (urn:li:activity:NNN of the post; the full permalink url is also accepted, from which the extractor derives the urn with the regex urn:li:activity:\\d+. The navigate uses {{urn}} directly, so pass it the urn)"}{"post":"object { author, authorSlug, authorHeadline, text (full), urn, url (permalink), reactions (int, total counter), comments (int from the counter), time }","reactors":"array of { name:string|null, slug:string|null (vanityName from /in/<slug>, for get-profile/connect), reaction:string|null (type: Like|Celebrate|Support|Love|Insightful|Funny), degree:string|null (1st/2nd/3rd+), headline:string|null }: who reacted to the post, from the Reactions dialog the skill opens; the visible top (~10, the dialog paginates on scroll), NOT the full list","reactionsSummary":"object { <type>: int } breakdown by reaction type over the returned reactors","comments":"array of { author, authorSlug, authorHeadline, text, time, reactions (int) } with the comments visible in the DOM"}Fetch ONE LinkedIn post with its full context (post + visible comments + visible REACTORS) from its urn (urn:li:activity:NNN) or its permalink url. Read-only. Useful to get context before replying to a comment or doing outreach, and to MINE SIGNALS: whoever reacted to the post (with their slug, to enrich via get-profile or act via connect / send-message) is a lead as strong as whoever commented. Navigates to the permalink /feed/update/<urn>/, waits for the comments to render (Voyager social detail XHR) and parses the post, the comment cards and the reactor facepile from the DOM. The param is urn: pass the urn directly; if you pass a permalink url, the extractor pulls the urn out of it with the regex urn:li:activity:\d+.
To get the reactors (who reacted / liked the post), the skill opens the "Reactions" dialog itself (a read-only click on the "N reactions" summary; it never adds a reaction) and reads each row: { name, slug, reaction (Like|Celebrate|Support|Love|Insightful|Funny), degree (1st/2nd/3rd+), headline }, plus reactionsSummary (breakdown by type). The slug lets you compose with linkedin.com/get-profile(slug) or act via connect / send-message. NOTE: this returns the visible top (~10); the dialog paginates on scroll, so it is NOT the full reactor list, but it is the actionable top for outreach.
params. It does the in-page work and returns the JSON in returns.( async (root, params) => { const doc = root.querySelector ? root : (root.ownerDocument || document); const txt = (el) => el ? (el.innerText || el.textContent || '').trim() : ''; const toInt = (s) => { if (s == null) return 0; const m = String(s).replace(/[.,\s]/g, '').match(/\d+/); return m ? parseInt(m[0], 10) : 0; }; const slugFromHref = (href) => { if (!href) return null; const m = href.match(//in/([^/?#]+)/); return m ? decodeURIComponent(m[1]) : null; }; const firstLine = (el) => { const t = txt(el); return t ? t.split('\n')[0].trim() : null; };
let urn = null; if (params && params.urn) { const m = String(params.urn).match(/urn:li:activity:\d+/); if (m) urn = m[0]; } if (!urn) { const aLink = doc.querySelector('a[href*="/analytics/post-summary/urn:li:activity:"]'); if (aLink) { const mm = aLink.getAttribute('href').match(/urn:li:activity:\d+/); if (mm) urn = mm[0]; } } if (!urn) { const du = doc.querySelector('[data-urn^="urn:li:activity:"], [data-id^="urn:li:activity:"]'); if (du) { const a = du.getAttribute('data-urn') || du.getAttribute('data-id'); const mm = a && a.match(/urn:li:activity:\d+/); if (mm) urn = mm[0]; } } const url = urn ? ('https://www.linkedin.com/feed/update/' + urn + '/') : ((typeof location !== 'undefined') ? location.href : null);
const main = doc.querySelector('main') || doc;
const optBtns = Array.from(main.querySelectorAll('button[aria-label*="omentario de"], button[aria-label*="omment"]')); const optSel = 'button[aria-label*="omentario de"], button[aria-label*="omment"]'; const optSelEsc = optSel; const cardOf = (btn) => { let n = btn, card = null; for (let i = 0; i < 12 && n && n !== main; i++) { const parent = n.parentElement; if (!parent || parent === main || parent === doc.body) { if (n.querySelector && n.querySelector('a[href*="/in/"]')) { const opts = Array.from(n.querySelectorAll(optSelEsc)); if (opts.length === 1 && !n.querySelector('a[href*="/analytics/post-summary/"]')) card = n; } break; } const opts = Array.from(parent.querySelectorAll(optSelEsc)); if (opts.length > 1) break; if (parent.querySelector('a[href*="/analytics/post-summary/"]')) break; n = parent; if (n.querySelector('a[href*="/in/"]')) card = n; } return card; }; const seen = new Set(); const comments = []; for (const btn of optBtns) { let card = cardOf(btn); if (!card || seen.has(card)) continue; seen.add(card); const inLinks0 = Array.from(card.querySelectorAll('a[href*="/in/"]')); const authorLink = inLinks0.find(a => a.querySelector('p, span') || txt(a)) || inLinks0[0] || null; const authorSlug = slugFromHref(authorLink ? authorLink.getAttribute('href') : null); let author = null; const al = btn.getAttribute('aria-label') || ''; const mName = al.match(/comentario de\s+(.+)$/i) || al.match(/comment from\s+(.+)$/i) || al.match(/comment of\s+(.+)$/i); if (mName) author = mName[1].trim(); if (!author && authorLink) { const nn = authorLink.querySelector('span[aria-hidden="true"]') || authorLink.querySelector('p, span'); author = firstLine(nn) || firstLine(authorLink); } if (author) author = author.split('•')[0].split('\n')[0].trim(); comments.push({ card, author, authorSlug }); }
const builtComments = comments.map(({ card, author, authorSlug }) => { const inLinks = Array.from(card.querySelectorAll('a[href*="/in/"]')); const authorLink = inLinks.find(a => a.querySelector('p') || txt(a)) || inLinks[0] || null; let authorHeadline = null; if (authorLink) { const ps = Array.from(authorLink.querySelectorAll('p')); if (ps.length >= 2) authorHeadline = txt(ps[ps.length - 1]); } let time = null; const allP = Array.from(card.querySelectorAll('p, span, time')); for (const p of allP) { const t = txt(p); if (/^\d+\s*(seg|min|h|hs|hora|d[ií]a|sem|mes|a[ñn]o|mo|w|d|y)/i.test(t) && t.length < 25) { time = t.replace(/\s*•.$/, '').trim(); break; } } const exclude = new Set([author, authorHeadline, time].filter(Boolean)); let text = null, best = 0; for (const p of allP) { if (p.closest('a')) continue; const t = txt(p); if (!t || exclude.has(t)) continue; if (/^\d+\s(seg|min|h|d[ií]a|sem|mes|a[ñn]o)/i.test(t) && t.length < 25) continue; if (/^(Responder|Reply|Me gusta|Like|Recomendar|Mostrar traducci)/i.test(t)) continue; if (t.length > best) { best = t.length; text = t; } } if (!authorHeadline) { for (const p of allP) { if (p.closest('a')) continue; const t = txt(p); if (!t || t === author || t === text || t === time) continue; if (/^\d+\s*(seg|min|h|d[ií]a|sem|mes|a[ñn]o)/i.test(t) && t.length < 25) continue; if (/^(Responder|Reply|Me gusta|Like|Recomendar|Mostrar traducci|\d+$)/i.test(t)) continue; if (t.length < 160) { authorHeadline = t; break; } } } let reactions = 0; const reBtns = Array.from(card.querySelectorAll('button')); for (const b of reBtns) { const t = txt(b).trim(); if (/^\d+$/.test(t)) { reactions = toInt(t); break; } } return { author, authorSlug, authorHeadline, text, time, reactions }; }).filter(c => c.author || c.text);
// ===== POST ===== const liSel = 'li, [role="listitem"]'; let postEl = null; const aurl = main.querySelector('a[href*="/analytics/post-summary/"]'); const aurlLi = aurl ? aurl.closest(liSel) : null; if (aurlLi && aurlLi.querySelector('a[href*="/in/"]') && !aurlLi.querySelector(optSel)) postEl = aurlLi; if (!postEl) { const lis = Array.from(main.querySelectorAll(liSel)); postEl = lis.find(li => li.querySelector('a[href*="/in/"]') && !li.querySelector(optSel)) || null; } if (!postEl) { const actor0 = main.querySelector('a[href*="/in/"]'); if (actor0) { let n = actor0; for (let i = 0; i < 14 && n && n !== main; i++) { const parent = n.parentElement; if (!parent || parent === main) break; if (parent.querySelector(optSel)) break; n = parent; } postEl = n; } } if (!postEl) postEl = main;
let author = null, authorSlug = null, authorHeadline = null, text = null, time = null; const pInLinks = Array.from(postEl.querySelectorAll('a[href*="/in/"]')); const actorLink = pInLinks.find(a => a.querySelector('p, span') || txt(a)) || pInLinks[0] || null; authorSlug = slugFromHref(actorLink ? actorLink.getAttribute('href') : null); const ctrlBtn = postEl.querySelector('button[aria-label*="publicaci"], button[aria-label*="control menu for"], button[aria-label*="post by"]'); if (ctrlBtn) { const cl = ctrlBtn.getAttribute('aria-label') || ''; const cm = cl.match(/publicaci[oó]n de\s+(.+)$/i) || cl.match(/post by\s+(.+)$/i) || cl.match(/\bde\s+(.+)$/i); if (cm) author = cm[1].trim(); } if (!author && actorLink) { const nameNode = actorLink.querySelector('span[aria-hidden="true"]') || actorLink.querySelector('p, span'); let raw = nameNode ? firstLine(nameNode) : firstLine(actorLink); if (raw) author = raw; } if (author) author = author.split('•')[0].split('\n')[0].trim(); let bestLen = 0; for (const p of Array.from(postEl.querySelectorAll('p'))) { if (p.closest('a')) continue; const t = txt(p); if (!t) continue; if (/impresiones|impressions/i.test(t)) continue; if (t.length > bestLen) { bestLen = t.length; text = t; } } const headerPnodes = Array.from(postEl.querySelectorAll('p')).filter(p => !(actorLink && actorLink.contains(p))); for (const p of headerPnodes) { const t = txt(p); if (!t) continue; const clean = t.replace(/\s*•.$/, '').trim(); if (author && (clean === author || t === author)) continue; if (t === text) continue; if (/^•/.test(t) || /^(Tú|You|1er|2do|3er|\d+\s(grado|°))/i.test(clean) && clean.length < 8) continue; if (/^\d+\s*(seg|min|h|d[ií]a|sem|mes|a[ñn]o|mo|w|d|y)/i.test(t) && t.length < 30) { if (!time) time = clean; continue; } if (!authorHeadline && t.length < 220 && !/impresiones|impressions|reacc|reaction|comentario|comment/i.test(t) && !/^\d+$/.test(t)) { authorHeadline = t; } } if (!authorHeadline && actorLink) { const lps = Array.from(actorLink.querySelectorAll('p')).map(txt).filter(Boolean); const cand = lps.find(t => { const c = t.replace(/\s*•.*$/, '').trim(); return c !== author && !/^•/.test(t) && !/^(Tú|You)$/i.test(c) && t.length > 2; }); if (cand) authorHeadline = cand; }
let reactions = 0; const reLink = postEl.querySelector('a[aria-label*="reacc"], a[aria-label*="reaction"], button[aria-label*="reacc"], button[aria-label*="reaction"]'); if (reLink) reactions = toInt(reLink.getAttribute('aria-label') || txt(reLink)); if (!reactions) { for (const el of Array.from(postEl.querySelectorAll('a, span'))) { const lbl = (el.getAttribute('aria-label') || '') + ' ' + txt(el); if (/reacc|reaction/i.test(lbl)) { const n = toInt(lbl); if (n) { reactions = n; break; } } } }
let commentsCount = null; for (const el of Array.from(postEl.querySelectorAll('span, a, button, p'))) { const lbl = (el.getAttribute('aria-label') || '') + ' ' + txt(el); if (/comentario|comment/i.test(lbl) && /\d/.test(lbl)) { const n = toInt(lbl); if (n) { commentsCount = n; break; } } } if (commentsCount == null) commentsCount = builtComments.length;
// ===== REACTORS (who reacted / liked the post) ===== // The reactor rows carry an aria-label like "<Name> reacted with <Type>, <Nth> degree connection, <Headline>". // They live in the "Reactions" dialog, which is NOT rendered inline, so open it first (read-only click on // the "N reactions" summary). Then parse the visible top rows (~10; the dialog paginates on scroll). const reLabelRe = /^(.?)\s+(?:reacted with|reaccion[oó] con|ha reaccionado con)\s+([A-Za-zÁÉÍÓÚáéíóúÑñ]+)/i; const hasReactorRows = () => Array.from(doc.querySelectorAll('[aria-label]')).some((el) => reLabelRe.test(el.getAttribute('aria-label') || '')); if (!hasReactorRows()) { const trig = Array.from(main.querySelectorAll('a, button')).find((e) => /^\d[\d.,]\s+reactions?$/i.test(txt(e)) || /^view all reactions/i.test(e.getAttribute('aria-label') || '')); if (trig) { const rr = trig.getBoundingClientRect(); const o = { bubbles: true, cancelable: true, composed: true, clientX: rr.left + rr.width / 2, clientY: rr.top + rr.height / 2, view: window }; for (const type of ['pointerover', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) { const C = type.indexOf('pointer') === 0 ? PointerEvent : MouseEvent; try { trig.dispatchEvent(new C(type, Object.assign({ pointerId: 1, isPrimary: true }, o))); } catch (e) {} } for (let i = 0; i < 20 && !hasReactorRows(); i++) { await new Promise((res) => setTimeout(res, 200)); } } } const reactors = []; const reactionsSummary = {}; const seenReactors = new Set(); for (const el of Array.from(doc.querySelectorAll('[aria-label]'))) { const lab = (el.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim(); const m = lab.match(reLabelRe); if (!m) continue; const link = el.querySelector('a[href*="/in/"]') || (el.closest ? el.closest('a[href*="/in/"]') : null); const slug = slugFromHref(link ? link.getAttribute('href') : null); if (!slug || seenReactors.has(slug)) continue; seenReactors.add(slug); const name = m[1].split('•')[0].trim() || null; const reaction = m[2].trim(); let degree = null, headline = null; const dm = lab.match(/,\s*(1st|2nd|3rd+?|\dth|1er|2do|3er)\s*(?:degree|grado)/i); if (dm) degree = dm[1]; const hm = lab.match(/degree connection,\s*(.+)$/i) || lab.match(/grado,\s*(.+)$/i); if (hm) headline = hm[1].trim(); reactionsSummary[reaction] = (reactionsSummary[reaction] || 0) + 1; reactors.push({ name, slug, reaction, degree, headline }); }
return { post: { author, authorSlug, authorHeadline, text, urn, url, reactions, comments: commentsCount, time }, reactors: reactors, reactionsSummary: reactionsSummary, comments: builtComments }; } )
{
"expr": "main button[aria-label*=\"omentario de\"], main button[aria-label*=\"omment\"], main a[href*=\"/analytics/post-summary/\"]",
"type": "dom"
}