Why Social Media Screenshots Are Hard
Social media platforms are hostile to automated capture. They load content dynamically via JavaScript, require authentication for most profiles, aggressively detect and block headless browsers, and serve different layouts based on viewport, geolocation, and login state. A naive page.screenshot() often captures a login wall or an empty shell.
| Platform | Main Challenge | Auth Required? |
|---|---|---|
| Twitter/X | Login wall after few views, rate limiting | Usually yes |
| Aggressive bot detection, Authwall | Yes | |
| Login required for most content | Yes | |
| Heavy JS rendering, privacy walls | Usually yes | |
| Old/new Reddit layouts, interstitials | No (public posts) | |
| YouTube | Consent dialogs, dynamic loading | No (public videos) |
Capturing Twitter/X Posts
Twitter has an official embed API that renders tweet cards without authentication. This is the most reliable approach — no login, no bot detection.
const { chromium } = require('playwright');
async function captureTweet(tweetUrl) {
const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
if (!tweetId) throw new Error('Invalid tweet URL');
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 550, height: 800 } });
// Use Twitter's embed endpoint — no auth needed
const embedHtml = `
<!DOCTYPE html>
<html><head><style>body{margin:0;background:#0a0a0f;display:flex;justify-content:center;padding:20px;}</style></head>
<body>
<blockquote class="twitter-tweet" data-theme="dark">
<a href="${tweetUrl}"></a>
</blockquote>
<script src="https://platform.twitter.com/widgets.js"></script>
</body></html>`;
await page.setContent(embedHtml);
// Wait for Twitter widget to render
await page.waitForSelector('iframe.twitter-tweet-rendered', { timeout: 15000 });
await page.waitForTimeout(2000); // Let images load
const frame = page.frames().find(f => f.url().includes('platform.twitter.com'));
const tweetElement = frame
? await frame.$('article')
: await page.$('.twitter-tweet-rendered');
const screenshot = tweetElement
? await tweetElement.screenshot()
: await page.screenshot({ fullPage: true });
await browser.close();
return screenshot;
}
For bulk captures, maintain a browser instance and reuse pages. Twitter rate-limits embed renders too — add 2-3 second delays between captures.
LinkedIn Profile Screenshots
LinkedIn is one of the hardest platforms to capture. It blocks headless browsers aggressively and requires authentication for profile views. The safest approach uses stored session cookies.
async function captureLinkedIn(profileUrl, cookiesPath) {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
storageState: cookiesPath, // Saved from a real browser session
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
viewport: { width: 1280, height: 900 }
});
const page = await context.newPage();
await page.goto(profileUrl, { waitUntil: 'networkidle' });
// Close any modals (LinkedIn loves modals)
const modal = await page.$('[data-test-modal-close-btn]');
if (modal) await modal.click();
await page.waitForTimeout(1000);
// Capture just the profile card
const profileCard = await page.$('.pv-top-card');
const screenshot = profileCard
? await profileCard.screenshot()
: await page.screenshot({ clip: { x: 0, y: 0, width: 1280, height: 600 } });
await browser.close();
return screenshot;
}
Export cookies from your browser using a cookie editor extension, save as Playwright's storageState JSON format. Sessions last ~30 days before re-auth is needed. Don't hit more than ~50 profiles/hour or you'll trigger LinkedIn's bot detection.
Multi-Platform Capture Class
Build a single class that handles different platforms with platform-specific wait strategies and element selectors.
class SocialCapture {
constructor() { this.browser = null; }
async init() {
this.browser = await chromium.launch();
}
async capture(url) {
const platform = this.detectPlatform(url);
const page = await this.browser.newPage({
viewport: platform.viewport
});
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
// Platform-specific waits
if (platform.waitSelector) {
await page.waitForSelector(platform.waitSelector, { timeout: 10000 })
.catch(() => {});
}
// Dismiss common overlays
for (const sel of platform.dismissSelectors || []) {
const el = await page.$(sel);
if (el) await el.click().catch(() => {});
}
await page.waitForTimeout(1500);
const element = platform.captureSelector
? await page.$(platform.captureSelector)
: null;
return element
? await element.screenshot()
: await page.screenshot({ fullPage: false });
} finally {
await page.close();
}
}
detectPlatform(url) {
const platforms = {
'twitter.com': { viewport: { width: 600, height: 900 },
waitSelector: 'article', captureSelector: 'article',
dismissSelectors: ['[data-testid="sheetDialog"] [role="button"]'] },
'x.com': { viewport: { width: 600, height: 900 },
waitSelector: 'article', captureSelector: 'article',
dismissSelectors: ['[data-testid="sheetDialog"] [role="button"]'] },
'linkedin.com': { viewport: { width: 1280, height: 900 },
waitSelector: '.feed-shared-update-v2',
dismissSelectors: ['[data-test-modal-close-btn]'] },
'reddit.com': { viewport: { width: 800, height: 1000 },
waitSelector: 'shreddit-post',
dismissSelectors: ['[id="accept-all"]', '.XPromoPopup button'] },
'youtube.com': { viewport: { width: 1280, height: 720 },
waitSelector: '#player', captureSelector: '#player',
dismissSelectors: ['[aria-label="Accept all"]', 'tp-yt-paper-dialog .yt-spec-button-shape-next'] }
};
const host = new URL(url).hostname.replace('www.', '');
return platforms[host] || { viewport: { width: 1280, height: 900 } };
}
async close() { if (this.browser) await this.browser.close(); }
}
SnapAPI: Skip the Complexity
All the platform-specific logic above is fragile — selectors change, bot detection evolves, sessions expire. SnapAPI handles all of this with stealth mode, cookie blocking, and automatic wait strategies.
// Capture any social media post — one API call
const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'sk_live_your_key'
},
body: JSON.stringify({
url: 'https://x.com/elonmusk/status/1234567890',
fullPage: false,
stealth: true,
blockAds: true,
blockCookieBanners: true,
device: 'desktop',
format: 'png'
})
});
const imageBuffer = await response.arrayBuffer();
fs.writeFileSync('tweet.png', Buffer.from(imageBuffer));
# Python — batch capture social posts
import httpx, asyncio
async def capture_social(urls):
async with httpx.AsyncClient(timeout=30) as client:
tasks = [
client.post('https://api.snapapi.pics/v1/screenshot',
headers={'X-Api-Key': 'sk_live_your_key'},
json={'url': url, 'stealth': True, 'blockAds': True, 'blockCookieBanners': True}
)
for url in urls
]
responses = await asyncio.gather(*tasks)
for i, resp in enumerate(responses):
if resp.status_code == 200:
with open(f'social_{i}.png', 'wb') as f:
f.write(resp.content)
asyncio.run(capture_social([
'https://x.com/OpenAI/status/123456',
'https://www.reddit.com/r/programming/comments/abc123/',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
]))
SnapAPI's stealth mode handles headless detection bypass, and the blockCookieBanners flag removes consent dialogs that would otherwise cover the content. 200 free requests/month, no browser infrastructure to maintain.