Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Created March 10, 2026 11:45
Show Gist options
  • Select an option

  • Save minanagehsalalma/9bd62eda8a59d09653c3b767e4e3cee3 to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/9bd62eda8a59d09653c3b767e4e3cee3 to your computer and use it in GitHub Desktop.
Tampermonkey userscript for Reddit — enter select mode, click any posts on the feed, then export them as structured JSON (subreddit, author, timestamp, title, body, flair, mod notices, link, upvotes, comments, views). Works on new Reddit (shreddit web components) and old Reddit. Useful for feeding Reddit content to LLMs.
// ==UserScript==
// @name Reddit Post JSON Exporter
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description Select Reddit posts and export them as clean JSON for LLM ingestion. Now captures rich text, bullets, flairs, and mod notices.
// @author You
// @match https://www.reddit.com/*
// @match https://old.reddit.com/*
// @grant GM_setClipboard
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ─── State ────────────────────────────────────────────────────────────────
let selectedPosts = [];
let selectMode = false;
// ─── Inject Styles ────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
#rpe-panel {
position: fixed;
bottom: 28px;
right: 28px;
z-index: 999999;
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
#rpe-badge {
background: #ff4500;
color: #fff;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
padding: 3px 9px;
border-radius: 99px;
display: none;
box-shadow: 0 2px 8px rgba(255,69,0,0.4);
}
#rpe-controls {
display: flex;
gap: 8px;
align-items: center;
}
.rpe-btn {
border: none;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.05em;
padding: 10px 16px;
transition: transform 0.1s, box-shadow 0.1s, opacity 0.15s;
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
}
.rpe-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,0,0,0.3); }
.rpe-btn:active { transform: translateY(0px); }
#rpe-toggle {
background: #1a1a2e;
color: #e8e8f0;
border: 1.5px solid #333355;
}
#rpe-toggle.active {
background: #ff4500;
color: #fff;
border-color: #ff4500;
box-shadow: 0 4px 14px rgba(255,69,0,0.45);
}
#rpe-export {
background: #0a7c5c;
color: #fff;
display: none;
}
#rpe-clear {
background: #2a2a2a;
color: #aaa;
display: none;
padding: 10px 12px;
}
body.rpe-mode .rpe-selectable {
outline: 2px dashed #ff450044;
outline-offset: 4px;
border-radius: 6px;
cursor: crosshair !important;
transition: outline-color 0.15s, background 0.15s;
}
body.rpe-mode .rpe-selectable:hover {
outline-color: #ff4500aa !important;
background: rgba(255, 69, 0, 0.03);
}
.rpe-selected {
outline: 2px solid #ff4500 !important;
outline-offset: 4px;
background: rgba(255, 69, 0, 0.06) !important;
border-radius: 6px;
}
#rpe-modal {
display: none;
position: fixed;
inset: 0;
z-index: 9999999;
background: rgba(0,0,0,0.72);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
}
#rpe-modal.open { display: flex; }
#rpe-modal-box {
background: #0f0f17;
border: 1.5px solid #2a2a40;
border-radius: 16px;
width: min(820px, 92vw);
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0,0,0,0.6);
}
#rpe-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 22px 14px;
border-bottom: 1px solid #1e1e30;
}
#rpe-modal-header h2 {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.12em;
color: #ff4500;
text-transform: uppercase;
}
#rpe-modal-actions {
display: flex;
gap: 8px;
}
.rpe-modal-btn {
border: none;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-weight: 700;
font-size: 11px;
letter-spacing: 0.07em;
padding: 7px 14px;
transition: opacity 0.15s;
}
.rpe-modal-btn:hover { opacity: 0.85; }
#rpe-copy-btn { background: #ff4500; color: #fff; }
#rpe-close-btn { background: #1e1e2e; color: #888; border: 1px solid #2a2a3e; }
#rpe-output {
flex: 1;
overflow-y: auto;
padding: 20px 22px;
margin: 0;
font-size: 12.5px;
line-height: 1.7;
color: #c8d0e0;
white-space: pre-wrap;
word-break: break-word;
background: transparent;
scrollbar-width: thin;
scrollbar-color: #333 transparent;
}
#rpe-toast {
position: fixed;
bottom: 90px;
right: 28px;
z-index: 9999998;
background: #1a1a2e;
color: #e0e0f0;
font-family: 'SF Mono', monospace;
font-size: 12px;
font-weight: 600;
padding: 10px 18px;
border-radius: 10px;
border: 1px solid #2a2a40;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
opacity: 0;
transform: translateY(6px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
}
#rpe-toast.show {
opacity: 1;
transform: translateY(0);
}
`;
document.head.appendChild(style);
// ─── Build UI ─────────────────────────────────────────────────────────────
const panel = document.createElement('div');
panel.id = 'rpe-panel';
panel.innerHTML = `
<div id="rpe-badge">0 selected</div>
<div id="rpe-controls">
<button class="rpe-btn" id="rpe-clear">&#x2715; Clear</button>
<button class="rpe-btn" id="rpe-export">&#x2B07; Export JSON</button>
<button class="rpe-btn" id="rpe-toggle">&#x229E; Select Posts</button>
</div>
`;
document.body.appendChild(panel);
const modal = document.createElement('div');
modal.id = 'rpe-modal';
modal.innerHTML = `
<div id="rpe-modal-box">
<div id="rpe-modal-header">
<h2>Reddit Post Export</h2>
<div id="rpe-modal-actions">
<button class="rpe-modal-btn" id="rpe-copy-btn">Copy to Clipboard</button>
<button class="rpe-modal-btn" id="rpe-close-btn">Close</button>
</div>
</div>
<pre id="rpe-output"></pre>
</div>
`;
document.body.appendChild(modal);
const toast = document.createElement('div');
toast.id = 'rpe-toast';
document.body.appendChild(toast);
const toggleBtn = document.getElementById('rpe-toggle');
const exportBtn = document.getElementById('rpe-export');
const clearBtn = document.getElementById('rpe-clear');
const badge = document.getElementById('rpe-badge');
const output = document.getElementById('rpe-output');
const copyBtn = document.getElementById('rpe-copy-btn');
const closeBtn = document.getElementById('rpe-close-btn');
// ─── Toast ────────────────────────────────────────────────────────────────
let toastTimer;
function showToast(msg) {
clearTimeout(toastTimer);
toast.textContent = msg;
toast.classList.add('show');
toastTimer = setTimeout(() => toast.classList.remove('show'), 2200);
}
// ─── Parse "117K", "1.2M", plain numbers ─────────────────────────────────
function parseCount(raw) {
if (raw === null || raw === undefined) return null;
const str = String(raw).trim().replace(/,/g, '').replace(/\s+/g, '');
if (!str || str === '-' || str === 'Vote') return null;
if (/^\d+$/.test(str)) return parseInt(str, 10);
const m = str.match(/^([\d.]+)([KkMmBb])$/);
if (m) {
const n = parseFloat(m[1]);
const s = m[2].toUpperCase();
if (s === 'K') return Math.round(n * 1e3);
if (s === 'M') return Math.round(n * 1e6);
if (s === 'B') return Math.round(n * 1e9);
}
return null;
}
// ─── Views extraction ─────────────────────────────────────────────────────
function extractViews(el) {
for (const attr of ['view-count', 'viewcount', 'views', 'post-view-count', 'post-views']) {
const v = el.getAttribute(attr);
if (v !== null && v !== '') return parseCount(v);
}
const fullText = (el.innerText || el.textContent || '').replace(/\s+/g, ' ');
const viewMatch = fullText.match(/([\d.,]+[KkMmBb]?)\s+views?/i);
if (viewMatch) {
return parseCount(viewMatch[1]);
}
return null;
}
// ─── Formatted Text Extraction (Preserves Line Breaks and Lists) ──────────
function getFormattedText(node) {
if (!node) return '';
const clone = node.cloneNode(true);
// Convert <li> items to bullet points if they aren't already formatted
clone.querySelectorAll('li').forEach(li => {
const text = li.textContent.trim();
if (text && !/^[-•*]\s/.test(text)) {
li.prepend('- ');
}
});
// Replace line breaks
clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
// Add trailing newlines to block elements to maintain separation
clone.querySelectorAll('p, div, li, h1, h2, h3, h4, h5, h6').forEach(el => {
el.appendChild(document.createTextNode('\n'));
});
// Condense multiple newlines and trim whitespace
return clone.textContent.replace(/\n[ \t]*\n+/g, '\n\n').trim();
}
// ─── Body + notices extraction ────────────────────────────────────────────
function extractBody(el) {
const parts = [];
// Flair (using shreddit specific elements/slots)
const flairEls = el.querySelectorAll(
'[data-testid="post-flair"], .flair, [class*="flair"], [slot="post-flair"], shreddit-post-flair'
);
const flairs = [...flairEls]
.map(f => f.textContent.trim())
.filter(f => f && f.length < 80);
// De-dupe and append flairs
if (flairs.length) parts.push('[Flair: ' + [...new Set(flairs)].join(' | ') + ']');
// Text post body (using formatted extraction)
const bodySelectors = [
'[slot="text-body"]',
'div[data-post-click-location="text-body"]',
'[data-click-id="text"] .md',
'.usertext-body .md'
];
for (const sel of bodySelectors) {
const node = el.querySelector(sel);
if (node) {
const txt = getFormattedText(node);
if (txt) { parts.push(txt); break; }
}
}
// Mod/removal/admin banners (using shreddit notice slots)
const noticeSelectors = [
'[slot="post-notice"]',
'shreddit-post-notice',
'[data-testid="mod-removed-snackbar"]',
'[class*="removal-reason"]',
'[class*="removed-banner"]',
'[class*="ModeratorBanner"]',
'shreddit-mod-removed-banner',
'faceplate-alert'
];
for (const sel of noticeSelectors) {
el.querySelectorAll(sel).forEach(node => {
const txt = getFormattedText(node).replace(/\n/g, ' ').trim();
if (txt && txt.length > 10 && !parts.some(p => p.includes(txt))) {
parts.push('[Notice: ' + txt + ']');
}
});
}
// Fallback broad text scan for known Reddit removal strings
const fullPostText = (el.innerText || el.textContent || '').replace(/\s+/g, ' ');
const removalRegexps = [
/(Sorry, this post has been removed by the moderators of r\/[^\s.]+)/i,
/(Sorry, this post was removed by Reddit's filters)/i,
/(This post was removed by Reddit's spam filters)/i,
/(This post was deleted by the person who originally posted it)/i
];
for (const re of removalRegexps) {
const match = fullPostText.match(re);
if (match && !parts.some(p => p.includes(match[1]))) {
parts.push('[Notice: ' + match[1] + ']');
}
}
return parts.length ? parts.join('\n\n') : null;
}
// ─── Main extractor ───────────────────────────────────────────────────────
function extractSubFromURL() {
const m = location.pathname.match(/\/r\/([^/]+)/);
return m ? 'r/' + m[1] : null;
}
function extractPostData(el) {
const tag = el.tagName?.toLowerCase();
// New Reddit web component (shreddit)
if (tag === 'shreddit-post') {
const sub = el.getAttribute('subreddit-prefixed-name')
|| el.getAttribute('subreddit-name')
|| extractSubFromURL();
const author = el.getAttribute('author')
|| el.querySelector('[data-testid="post_author_link"]')?.textContent?.trim() || null;
const created = el.getAttribute('created-timestamp')
|| el.getAttribute('post-created-at') || null;
const title = el.getAttribute('post-title')
|| el.querySelector('[slot="title"]')?.textContent?.trim() || null;
const permalink = el.getAttribute('permalink') || null;
const score = el.getAttribute('score') || el.getAttribute('upvotes') || null;
const cmtCount = el.getAttribute('comment-count') || null;
const views = extractViews(el);
const body = extractBody(el);
const linkEl = el.querySelector(
'a[data-testid="outbound-link"], a[slot="outbound-link"], a[data-post-click-location="outbound-link"]'
);
const link = linkEl?.href || (permalink ? 'https://www.reddit.com' + permalink : null);
return {
subreddit: sub,
author: author,
posted_at: created ? new Date(created).toISOString() : null,
title,
body,
link,
permalink: permalink ? 'https://www.reddit.com' + permalink : null,
upvotes: parseCount(score),
comments: parseCount(cmtCount),
views,
};
}
// Article / old Reddit fallback
const sub = el.querySelector('.subreddit')?.textContent?.trim()
|| el.querySelector('[data-subreddit]')?.dataset?.subreddit
|| document.querySelector('meta[name="reddit:subreddit"]')?.content
|| extractSubFromURL();
const authorEl = el.querySelector('.author, [data-testid="post_author_link"]');
const author = authorEl?.textContent?.trim() || null;
const timeEl = el.querySelector('time');
const posted = timeEl?.getAttribute('datetime') || timeEl?.title || null;
const titleEl = el.querySelector('a[data-click-id="body"], h3, .title a, h1');
const title = titleEl?.textContent?.trim() || null;
const scoreEl = el.querySelector('[data-score], .score, .upvoteCount');
const upvotes = parseCount(scoreEl?.getAttribute('data-score') || scoreEl?.textContent);
const cmtEl = el.querySelector('[data-testid="post-comment-count"], .comments, a[data-click-id="comments"]');
const comments = parseCount((cmtEl?.textContent || '').replace(/[^0-9KkMm.]/g, '').trim());
const views = extractViews(el);
const body = extractBody(el);
const extLink = el.querySelector('a[data-testid="outbound-link"], a.outbound-link');
const permalink = el.querySelector('a[data-click-id="body"]')?.href
|| el.querySelector('a.title')?.href || null;
return {
subreddit: sub,
author: author,
posted_at: posted ? new Date(posted).toISOString() : null,
title,
body,
link: extLink?.href || permalink || null,
permalink: permalink || null,
upvotes: isNaN(upvotes) ? null : upvotes,
comments: isNaN(comments) ? null : comments,
views,
};
}
// ─── Select mode ──────────────────────────────────────────────────────────
function getPostElements() {
return [
...document.querySelectorAll('shreddit-post'),
...document.querySelectorAll('article[data-testid="post-container"]'),
...document.querySelectorAll('.thing.link'),
];
}
function updateBadge() {
badge.textContent = selectedPosts.length + ' selected';
badge.style.display = selectedPosts.length ? 'block' : 'none';
exportBtn.style.display = selectedPosts.length ? 'inline-block' : 'none';
clearBtn.style.display = selectedPosts.length ? 'inline-block' : 'none';
}
function attachListeners() {
getPostElements().forEach(el => {
if (el._rpeAttached) return;
el._rpeAttached = true;
el.classList.add('rpe-selectable');
el.addEventListener('click', function (e) {
if (!selectMode) return;
e.preventDefault();
e.stopPropagation();
const idx = selectedPosts.findIndex(p => p._el === el);
if (idx === -1) {
el.classList.add('rpe-selected');
const data = extractPostData(el);
selectedPosts.push({ _el: el, ...data });
showToast('Added: ' + (data.title || 'post').slice(0, 50) + '...');
} else {
el.classList.remove('rpe-selected');
selectedPosts.splice(idx, 1);
showToast('Deselected post');
}
updateBadge();
}, true);
});
}
const observer = new MutationObserver(() => { if (selectMode) attachListeners(); });
observer.observe(document.body, { childList: true, subtree: true });
// ─── Toggle ───────────────────────────────────────────────────────────────
toggleBtn.addEventListener('click', () => {
selectMode = !selectMode;
toggleBtn.classList.toggle('active', selectMode);
toggleBtn.textContent = selectMode ? 'Stop Selecting' : 'Select Posts';
document.body.classList.toggle('rpe-mode', selectMode);
if (selectMode) {
attachListeners();
showToast('Click any post to select it');
} else {
showToast('Selection mode off');
}
});
// ─── Export ───────────────────────────────────────────────────────────────
exportBtn.addEventListener('click', () => {
const clean = selectedPosts.map(({ _el, ...rest }) => rest);
output.textContent = JSON.stringify(
{ exported_at: new Date().toISOString(), count: clean.length, posts: clean },
null, 2
);
modal.classList.add('open');
});
clearBtn.addEventListener('click', () => {
selectedPosts.forEach(p => p._el?.classList.remove('rpe-selected'));
selectedPosts = [];
updateBadge();
showToast('Cleared all selections');
});
copyBtn.addEventListener('click', () => {
const text = output.textContent;
if (typeof GM_setClipboard !== 'undefined') {
GM_setClipboard(text);
} else {
navigator.clipboard.writeText(text).catch(() => {});
}
showToast('Copied to clipboard!');
});
closeBtn.addEventListener('click', () => modal.classList.remove('open'));
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('open'); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') modal.classList.remove('open'); });
})();
@minanagehsalalma
Copy link
Author

{
  "exported_at": "2026-03-10T09:14:33.201Z",
  "count": 4,
  "posts": [
    {
      "subreddit": "r/homelab",
      "author": "u/packet_goblin",
      "posted_at": "2026-03-09T21:44:11.000Z",
      "title": "Finally finished my basement server rack after 3 years of cable spaghetti — pic inside",
      "body": "[Flair: Showcase]\n\nThis started as a single Raspberry Pi in 2021. Somehow I now have 47U of gear, a UPS that could power a small office, and a wife who has stopped asking questions.\n\n- 3x Dell R730 (picked up for $180 each off eBay)\n- Ubiquiti Dream Machine Pro\n- Custom 3D-printed cable management panels\n- Ambient temp sitting at a toasty 31°C — next project is proper airflow",
      "link": "https://i.redd.it/abc123fake.jpg",
      "permalink": "https://www.reddit.com/r/homelab/comments/fakeabc/finally_finished_my_basement_server_rack/",
      "upvotes": 4821,
      "comments": 312,
      "views": 98400
    },
    {
      "subreddit": "r/wallstreetbets",
      "author": "u/YOLOcaust_Survivor",
      "posted_at": "2026-03-10T06:02:58.000Z",
      "title": "Turned $800 into $34k in 11 days then back into $800. AMA.",
      "body": "[Flair: Loss Porn]\n\nI am at peace.\n\nThe journey was beautiful. The 0DTE calls were beautiful. The margin call at 6:47am was less beautiful but I respect the consistency.\n\nAsking my parents for my old bedroom back tonight.",
      "link": null,
      "permalink": "https://www.reddit.com/r/wallstreetbets/comments/fakexyz/turned_800_into_34k_in_11_days/",
      "upvotes": 61200,
      "comments": 2847,
      "views": 1400000
    },
    {
      "subreddit": "r/LocalLLaMA",
      "author": "u/gguf_whisperer",
      "posted_at": "2026-03-10T03:19:45.000Z",
      "title": "Gemma 4B Q4_K_M is genuinely beating GPT-4o on my coding benchmarks and I don't know how to feel",
      "body": "[Flair: Discussion]\n\nRunning on a single 3080 with llama.cpp. Tested against my personal suite of 40 real-world dev tasks (not synthetic benchmarks).\n\nResults:\n- Gemma 4B Q4_K_M: 31/40\n- GPT-4o (API): 29/40\n- Claude Sonnet: 34/40\n\nThe quantization loss is negligible at this task type. Local inference is genuinely here. I'm not going back.",
      "link": null,
      "permalink": "https://www.reddit.com/r/LocalLLaMA/comments/fakelm/gemma_4b_q4_k_m_is_genuinely_beating_gpt4o/",
      "upvotes": 1893,
      "comments": 441,
      "views": 52100
    },
    {
      "subreddit": "r/tifu",
      "author": "u/definitely_not_fired",
      "posted_at": "2026-03-09T17:55:30.000Z",
      "title": "TIFU by accidentally deploying to prod during a live investor demo",
      "body": "[Flair: NSFW]\n\nSo this happened about 6 hours ago and I am still shaking.\n\nContext: I'm a backend dev at a Series B startup. Today we had 12 investors on a Zoom call watching our CTO demo the product live.\n\nI was supposed to deploy to staging. I had two terminal tabs open.\n\nThe site went down for 4 minutes. The loading spinner, which I had just changed to a placeholder as a joke, read: \"hang on lmao\".\n\nIt was on screen. In front of the investors. For four minutes.\n\nUpdate: still employed. The lead investor thought it was \"authentic\". We closed the round.\n\n[Notice: Sorry, this post has been removed by the moderators of r/tifu.]",
      "link": null,
      "permalink": "https://www.reddit.com/r/tifu/comments/faketifu/tifu_by_accidentally_deploying_to_prod/",
      "upvotes": 38700,
      "comments": 1204,
      "views": 887000
    }
  ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment