Drag this to your bookmark bar:
linkedin.com/feed/update/...)# LinkedIn Post
**Author:** Alex Morgan
**Posted:** 2d ago
**URL:** https://www.linkedin.com/feed/update/urn:li:activity:1234567890/
**URN:** urn:li:activity:1234567890
---
Post content goes here...
---
## Comments (3)
**Jordan Lee** — 1d ago
> Great post! I've been thinking about this too.
**Sam Chen** — 12h ago
> This is exactly what we need.
// LinkedIn Post to Markdown Bookmarklet
// Expands all comments, scrapes post + comments, copies as markdown
const L = console.log.bind(console);
const S = (selectors, root) => {
for (const s of selectors) {
const el = root.querySelector(s);
if (el) { L('[li2md] matched:', s); return el; }
}
L('[li2md] no match in:', selectors);
return null;
};
const A = (selectors, root) => {
const all = [];
for (const s of selectors) {
const found = root.querySelectorAll(s);
if (found.length) L('[li2md] matched', found.length, 'for:', s);
all.push(...found);
}
return all;
};
const T = el => el ? el.textContent.trim().replace(/\s+/g, ' ') : '';
// Anonymization: maps real names → fake names from a pool
const fakeNames = [
'Alex Morgan', 'Jordan Lee', 'Sam Chen', 'Riley Park',
'Casey Kim', 'Quinn Davis', 'Avery Brooks', 'Drew Santos',
'Jamie Walsh', 'Taylor Reed', 'Morgan Scott', 'Blake Turner',
'Reese Grant', 'Hayden Cole', 'Ellis Hart', 'Rowan Pierce',
'Sage Ellis', 'Jules Frost', 'Finley Shaw', 'Kit Palmer'
];
const nameMap = {}; // real name → fake name
let nameCount = 0; // must be let, not const (incremented)
function anon(name) {
if (!name || name === 'Unknown') return 'Unknown';
if (!nameMap[name]) {
nameMap[name] = fakeNames[nameCount % fakeNames.length];
nameCount++;
}
return nameMap[name];
}
function anonymizeText(text) {
for (const [real, alias] of Object.entries(nameMap)) {
text = text.split(real).join(alias);
}
return text;
}
function notify(msg) {
let d = document.getElementById('__li2md');
if (!d) {
d = document.createElement('div');
d.id = '__li2md';
Object.assign(d.style, {
position: 'fixed', top: '12px', right: '12px',
zIndex: '999999', background: '#0a66c2', color: '#fff',
padding: '10px 18px', borderRadius: '8px',
fontFamily: 'system-ui', fontSize: '14px', fontWeight: '600',
boxShadow: '0 2px 12px rgba(0,0,0,.3)', transition: 'opacity .3s'
});
document.body.appendChild(d);
}
d.style.opacity = '1';
d.textContent = msg;
}
function done(msg) {
notify(msg);
setTimeout(() => {
const d = document.getElementById('__li2md');
if (d) d.style.opacity = '0';
}, 3000);
}
async function expandAll() {
const maxRounds = 20;
for (let i = 0; i < maxRounds; i++) {
const btns = [];
document.querySelectorAll('button').forEach(b => {
const t = b.textContent.trim().toLowerCase();
if (
((t.includes('comment') || t.includes('repl')) &&
(t.includes('more') || t.includes('load') || t.includes('previous')))
|| t === 'see more replies'
) btns.push(b);
});
// Also expand truncated text
const showMore = document.querySelectorAll(
'button.feed-shared-inline-show-more-text__see-more-less-toggle'
);
btns.push(...showMore);
if (btns.length === 0) break;
notify('Expanding... round ' + (i + 1));
for (const b of btns) {
b.scrollIntoView({ block: 'center' });
['pointerover','pointerenter','pointerdown','pointerup','click']
.forEach(type => b.dispatchEvent(new PointerEvent(type, { bubbles: true })));
await new Promise(r => setTimeout(r, 300));
}
await new Promise(r => setTimeout(r, 800));
}
await new Promise(r => setTimeout(r, 500));
}
function scrapePost() {
const post = document.querySelector('div.feed-shared-update-v2[data-urn]')
|| document.querySelector('.scaffold-finite-scroll__content')
|| document.body;
// Try semantic selectors first, then SDUI aria-label fallback
let authorEl = S([
'span.update-components-actor__title',
'span.update-components-actor__name',
'.profile-rail-card__name span[aria-hidden=true]'
], post);
if (!authorEl) {
const ctl = document.querySelector('[aria-label^="Open control menu for post by"]');
if (ctl) {
const m = ctl.getAttribute('aria-label').match(/post by (.+)/);
if (m) authorEl = { textContent: m[1] };
}
}
const author = T(authorEl) || 'Unknown Author';
const headline = T(S([
'span.update-components-actor__description',
'.profile-rail-card__description span[aria-hidden=true]'
], post)) || '';
const time = T(S([
'span.update-components-actor__sub-description span[aria-hidden=true]',
'span.update-components-actor__sub-description',
'time'
], post)) || '';
const body = T(S([
'div.update-components-text.relative',
'div.update-components-text',
'div.feed-shared-update-v2__commentary span[dir=ltr]',
'[data-testid="expandable-text-box"]', // SDUI fallback
'span[dir=ltr]'
], post)) || '';
L('[li2md] author:', author, '| body length:', body.length);
const urn = window.location.href.match(/activity:(\d+)/);
const postUrl = window.location.href.split('?')[0];
let md = '# LinkedIn Post\n\n';
md += '**Author:** ' + anon(author) + '\n';
if (time) md += '**Posted:** ' + time + '\n';
md += '**URL:** ' + postUrl + '\n';
if (urn) md += '**URN:** urn:li:activity:' + urn[1] + '\n';
md += '\n---\n\n' + anonymizeText(body) + '\n\n---\n\n';
return md;
}
function scrapeComments() {
const commentEls = A([
'.comments-comment-item',
'.comments-comments-list__comment-item',
'article.comments-comment-item',
'.comments-comment-entity'
], document);
if (commentEls.length === 0) return '*No comments found.*\n';
let md = '## Comments (' + commentEls.length + ')\n\n';
const seen = new Set();
commentEls.forEach(c => {
const name = T(S([
'.comments-post-meta__name-text span',
'.comments-post-meta__actor-link span',
'span.comments-comment-meta__description-title',
'.comments-comment-meta__name'
], c)) || 'Unknown';
const anonName = anon(name);
let text = T(S([
'.comments-comment-item-content-body',
'.comments-comment-item__main-content',
'span[dir=ltr]'
], c)) || '';
const ts = T(S(['time.comments-comment-meta__data', 'time'], c)) || '';
const key = anonName + '|' + text.substring(0, 50);
if (seen.has(key)) return;
seen.add(key);
text = anonymizeText(text);
md += '**' + anonName + '**';
if (ts) md += ' — ' + ts;
md += '\n> ' + text.replace(/\n/g, '\n> ') + '\n\n';
});
return md;
}
async function run() {
notify('Expanding comments...');
await expandAll();
notify('Scraping post...');
const postMd = scrapePost();
const commentsMd = scrapeComments();
const full = postMd + commentsMd;
// Save as file download with author slug in filename
const urn = window.location.href.match(/activity:(\d+)/);
const urnSlug = urn ? urn[1] : 'post';
let authorEl2 = document.querySelector('span.update-components-actor__title')
|| document.querySelector('span.update-components-actor__name');
if (!authorEl2) {
const ctl = document.querySelector('[aria-label^="Open control menu for post by"]');
if (ctl) {
const m = ctl.getAttribute('aria-label').match(/post by (.+)/);
if (m) authorEl2 = { textContent: m[1] };
}
}
const authorName = authorEl2 ? authorEl2.textContent.trim() : 'unknown';
const authorSlug = authorName.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase()
.replace(/^-|-$/g, '');
const d = new Date();
const ymd = '' + d.getFullYear() + String(d.getMonth()+1).padStart(2,'0')
+ String(d.getDate()).padStart(2,'0');
const blob = new Blob([full], { type: 'text/markdown' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = ymd + '-linkedin-' + authorSlug + '-' + urnSlug + '.md';
a.click();
done('Saved: ' + a.download + ' (' + full.length + ' chars)');
}
run();