Remove clutter from Reddit: search telemetry links, ads, promoted posts, related content sections, and homepage search
// ==UserScript==
// @name Declutter Reddit
// @namespace August4067
// @version 0.0.1
// @description Remove clutter from Reddit: search telemetry links, ads, promoted posts, related content sections, and homepage search
// @author August4067
// @license MIT
// @match https://www.reddit.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @icon https://www.reddit.com/favicon.ico
// ==/UserScript==
(function () {
"use strict";
// Configuration
const CONFIG = {
selectors: {
searchTracker: "search-telemetry-tracker",
searchLink: 'a[href^="/search/"]',
relatedAnswers: 'aside[id^="answers-suggested-queries"]',
promotion: 'aside[id="right-rail-experience-root"]',
relatedPosts: 'aside[aria-label="Related Posts Section"]',
commentTreeAd: "shreddit-comment-tree-ad",
adPost: "shreddit-ad-post",
sidebarAd: "shreddit-async-loader[bundlename='sidebar_ad']",
recentPosts: "recent-posts",
searchHero: "#search-hero",
resourcesSection: "faceplate-expandable-section-helper",
advertiseButton: "advertise-button",
},
debug: true,
};
// Settings with defaults
const Settings = {
get removeRelatedAnswers() {
return GM_getValue("removeRelatedAnswers", true);
},
set removeRelatedAnswers(value) {
GM_setValue("removeRelatedAnswers", value);
},
get removeTopPosts() {
return GM_getValue("removeTopPosts", true);
},
set removeTopPosts(value) {
GM_setValue("removeTopPosts", value);
},
get removeRelatedPosts() {
return GM_getValue("removeRelatedPosts", true);
},
set removeRelatedPosts(value) {
GM_setValue("removeRelatedPosts", value);
},
get removeSearchHero() {
return GM_getValue("removeSearchHero", true);
},
set removeSearchHero(value) {
GM_setValue("removeSearchHero", value);
},
get removeRecentPosts() {
return GM_getValue("removeRecentPosts", true);
},
set removeRecentPosts(value) {
GM_setValue("removeRecentPosts", value);
},
};
// Utility: Debug logger
function debug(message, ...args) {
if (CONFIG.debug) {
console.log(`[Declutter Reddit] ${message}`, ...args);
}
}
// Core declutter functions
const Declutterer = {
/**
* Remove search telemetry tracker and replace with plain text
*/
removeSearchLink(tracker) {
const link = tracker.querySelector(CONFIG.selectors.searchLink);
if (!link) return false;
// Extract text content (excluding SVG icon)
const textContent = Array.from(link.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE)
.map((node) => node.textContent)
.join("")
.trim();
if (!textContent) return false;
// Replace tracker with plain text
const textNode = document.createTextNode(textContent);
tracker.parentNode.replaceChild(textNode, tracker);
debug(`Removed search link: "${textContent}"`);
return true;
},
/**
* Process all search telemetry trackers on the page
*/
processSearchLinks() {
const trackers = document.querySelectorAll(
CONFIG.selectors.searchTracker,
);
let count = 0;
trackers.forEach((tracker) => {
if (this.removeSearchLink(tracker)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} search link(s)`);
}
return count;
},
/**
* Remove Related Answers section
*/
removeRelatedAnswers(element) {
element.remove();
debug(`Removed Related Answers section`);
return true;
},
/**
* Process all Related Answers sections on the page
*/
processRelatedAnswers() {
if (!Settings.removeRelatedAnswers) return 0;
const sections = document.querySelectorAll(
CONFIG.selectors.relatedAnswers,
);
let count = 0;
sections.forEach((section) => {
if (this.removeRelatedAnswers(section)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Related Answers section(s)`);
}
return count;
},
/**
* Remove Top Posts section
*/
removeTopPosts(element) {
element.remove();
debug(`Removed Top Posts section`);
return true;
},
/**
* Process all Top Posts sections on the page
*/
processTopPosts() {
if (!Settings.removeTopPosts) return 0;
// Find Top Posts sections by finding h2 with "Top Posts" text
const headers = Array.from(document.querySelectorAll("h2")).filter((h2) =>
h2.textContent.trim().toUpperCase().includes("TOP POSTS"),
);
let count = 0;
headers.forEach((header) => {
// Find the parent container (should be a div.px-md or similar)
const container = header.closest("div");
if (container && this.removeTopPosts(container)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Top Posts section(s)`);
}
return count;
},
/**
* Remove Promotion section
*/
removePromotion(element) {
element.remove();
debug(`Removed Promotion section`);
return true;
},
/**
* Process all Promotion sections on the page
*/
processPromotions() {
const sections = document.querySelectorAll(CONFIG.selectors.promotion);
let count = 0;
sections.forEach((section) => {
if (this.removePromotion(section)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Promotion section(s)`);
}
return count;
},
/**
* Remove Related Posts section
*/
removeRelatedPosts(element) {
element.remove();
debug(`Removed Related Posts section`);
return true;
},
/**
* Process all Related Posts sections on the page
*/
processRelatedPosts() {
if (!Settings.removeRelatedPosts) return 0;
const sections = document.querySelectorAll(CONFIG.selectors.relatedPosts);
let count = 0;
sections.forEach((section) => {
if (this.removeRelatedPosts(section)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Related Posts section(s)`);
}
return count;
},
/**
* Remove Comment Tree Ad
*/
removeCommentTreeAd(element) {
element.remove();
debug(`Removed Comment Tree Ad`);
return true;
},
/**
* Process all Comment Tree Ads on the page
*/
processCommentTreeAds() {
const ads = document.querySelectorAll(CONFIG.selectors.commentTreeAd);
let count = 0;
ads.forEach((ad) => {
if (this.removeCommentTreeAd(ad)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Comment Tree Ad(s)`);
}
return count;
},
/**
* Remove Ad Post
*/
removeAdPost(element) {
element.remove();
debug(`Removed Ad Post`);
return true;
},
/**
* Process all Ad Posts on the page
*/
processAdPosts() {
const ads = document.querySelectorAll(CONFIG.selectors.adPost);
let count = 0;
ads.forEach((ad) => {
if (this.removeAdPost(ad)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Ad Post(s)`);
}
return count;
},
/**
* Remove Sidebar Ad
*/
removeSidebarAd(element) {
element.remove();
debug(`Removed Sidebar Ad`);
return true;
},
/**
* Process all Sidebar Ads on the page
*/
processSidebarAds() {
const ads = document.querySelectorAll(CONFIG.selectors.sidebarAd);
let count = 0;
ads.forEach((ad) => {
if (this.removeSidebarAd(ad)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Sidebar Ad(s)`);
}
return count;
},
/**
* Remove Recent Posts section
*/
removeRecentPosts(element) {
element.remove();
debug(`Removed Recent Posts section`);
return true;
},
/**
* Process all Recent Posts sections on the page
*/
processRecentPosts() {
if (!Settings.removeRecentPosts) return 0;
const sections = document.querySelectorAll(CONFIG.selectors.recentPosts);
let count = 0;
sections.forEach((section) => {
if (this.removeRecentPosts(section)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Recent Posts section(s)`);
}
return count;
},
/**
* Remove Search Hero section
*/
removeSearchHero(element) {
element.remove();
debug(`Removed Search Hero section`);
return true;
},
/**
* Process Search Hero section on the page
*/
processSearchHero() {
if (!Settings.removeSearchHero) return 0;
const section = document.querySelector(CONFIG.selectors.searchHero);
if (section && this.removeSearchHero(section)) {
debug(`Processed Search Hero section`);
return 1;
}
return 0;
},
/**
* Close Resources section
*/
closeResourcesSection(element) {
// Remove open attribute from faceplate-expandable-section-helper
element.removeAttribute("open");
// Find and close the details element within
const details = element.querySelector("details");
if (details) {
details.removeAttribute("open");
}
debug(`Closed Resources section`);
return true;
},
/**
* Process Resources section on the page
*/
processResourcesSection() {
const sections = document.querySelectorAll(
CONFIG.selectors.resourcesSection,
);
let count = 0;
sections.forEach((section) => {
// Check if this section contains the RESOURCES control
const summary = section.querySelector(
'summary [aria-controls="RESOURCES"]',
);
if (
summary &&
section.hasAttribute("open") &&
this.closeResourcesSection(section)
) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Resources section(s)`);
}
return count;
},
/**
* Remove Advertise Button
*/
removeAdvertiseButton(element) {
element.remove();
debug(`Removed Advertise Button`);
return true;
},
/**
* Process all Advertise Buttons on the page
*/
processAdvertiseButtons() {
const buttons = document.querySelectorAll(
CONFIG.selectors.advertiseButton,
);
let count = 0;
buttons.forEach((button) => {
if (this.removeAdvertiseButton(button)) {
count++;
}
});
if (count > 0) {
debug(`Processed ${count} Advertise Button(s)`);
}
return count;
},
/**
* Process all declutter operations
*/
processAll() {
this.processSearchLinks();
this.processRelatedAnswers();
this.processTopPosts();
this.processPromotions();
this.processRelatedPosts();
this.processCommentTreeAds();
this.processAdPosts();
this.processSidebarAds();
this.processRecentPosts();
this.processSearchHero();
this.processResourcesSection();
this.processAdvertiseButtons();
},
};
// Mutation observer for dynamic content
const ContentObserver = {
observer: null,
/**
* Check if mutation contains elements we want to process
*/
shouldProcessMutation(mutation) {
if (mutation.addedNodes.length === 0) return false;
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
// Check for search trackers
if (
node.tagName === "SEARCH-TELEMETRY-TRACKER" ||
node.querySelector?.(CONFIG.selectors.searchTracker)
) {
return true;
}
// Check for related answers sections
if (
Settings.removeRelatedAnswers &&
(node.matches?.(CONFIG.selectors.relatedAnswers) ||
node.querySelector?.(CONFIG.selectors.relatedAnswers))
) {
return true;
}
// Check for top posts sections (h2 with "Top Posts" text)
if (Settings.removeTopPosts) {
const h2Elements = node.querySelectorAll?.("h2") || [];
for (const h2 of h2Elements) {
if (h2.textContent.trim().toUpperCase().includes("TOP POSTS")) {
return true;
}
}
// Check if the node itself is an h2
if (
node.tagName === "H2" &&
node.textContent.trim().toUpperCase().includes("TOP POSTS")
) {
return true;
}
}
// Check for promotion sections (always remove)
if (
node.matches?.(CONFIG.selectors.promotion) ||
node.querySelector?.(CONFIG.selectors.promotion)
) {
return true;
}
// Check for related posts sections
if (
Settings.removeRelatedPosts &&
(node.matches?.(CONFIG.selectors.relatedPosts) ||
node.querySelector?.(CONFIG.selectors.relatedPosts))
) {
return true;
}
// Check for comment tree ads (always remove)
if (
node.tagName === "SHREDDIT-COMMENT-TREE-AD" ||
node.querySelector?.(CONFIG.selectors.commentTreeAd)
) {
return true;
}
// Check for ad posts (always remove)
if (
node.tagName === "SHREDDIT-AD-POST" ||
node.querySelector?.(CONFIG.selectors.adPost)
) {
return true;
}
// Check for sidebar ads (always remove)
if (
node.tagName === "SHREDDIT-ASYNC-LOADER" &&
node.getAttribute?.("bundlename") === "sidebar_ad"
) {
return true;
}
if (node.querySelector?.(CONFIG.selectors.sidebarAd)) {
return true;
}
// Check for recent posts sections
if (
Settings.removeRecentPosts &&
(node.tagName === "RECENT-POSTS" ||
node.querySelector?.(CONFIG.selectors.recentPosts))
) {
return true;
}
// Check for search hero section
if (
Settings.removeSearchHero &&
(node.id === "search-hero" ||
node.querySelector?.(CONFIG.selectors.searchHero))
) {
return true;
}
// Check for Resources section (always close)
if (
node.tagName === "FACEPLATE-EXPANDABLE-SECTION-HELPER" &&
node.querySelector?.('summary [aria-controls="RESOURCES"]')
) {
return true;
}
// Check for advertise buttons (always remove)
if (
node.tagName === "ADVERTISE-BUTTON" ||
node.querySelector?.(CONFIG.selectors.advertiseButton)
) {
return true;
}
}
return false;
},
/**
* Handle mutations
*/
handleMutations(mutations) {
const shouldProcess = mutations.some((mutation) =>
this.shouldProcessMutation(mutation),
);
if (shouldProcess) {
Declutterer.processAll();
}
},
/**
* Start observing document
*/
start() {
this.observer = new MutationObserver((mutations) =>
this.handleMutations(mutations),
);
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
debug("Content observer started");
},
};
// Menu commands
function setupMenu() {
GM_registerMenuCommand(
`${Settings.removeRelatedAnswers ? "✓" : "✗"} Remove Related Answers`,
() => {
Settings.removeRelatedAnswers = !Settings.removeRelatedAnswers;
const state = Settings.removeRelatedAnswers ? "enabled" : "disabled";
alert(`Related Answers removal ${state}. Refresh the page to apply.`);
},
);
GM_registerMenuCommand(
`${Settings.removeTopPosts ? "✓" : "✗"} Remove Top Posts`,
() => {
Settings.removeTopPosts = !Settings.removeTopPosts;
const state = Settings.removeTopPosts ? "enabled" : "disabled";
alert(`Top Posts removal ${state}. Refresh the page to apply.`);
},
);
GM_registerMenuCommand(
`${Settings.removeRelatedPosts ? "✓" : "✗"} Remove Related Posts`,
() => {
Settings.removeRelatedPosts = !Settings.removeRelatedPosts;
const state = Settings.removeRelatedPosts ? "enabled" : "disabled";
alert(`Related Posts removal ${state}. Refresh the page to apply.`);
},
);
GM_registerMenuCommand(
`${Settings.removeRecentPosts ? "✓" : "✗"} Remove Recent Posts`,
() => {
Settings.removeRecentPosts = !Settings.removeRecentPosts;
const state = Settings.removeRecentPosts ? "enabled" : "disabled";
alert(`Recent Posts removal ${state}. Refresh the page to apply.`);
},
);
GM_registerMenuCommand(
`${Settings.removeSearchHero ? "✓" : "✗"} Remove Homepage Search`,
() => {
Settings.removeSearchHero = !Settings.removeSearchHero;
const state = Settings.removeSearchHero ? "enabled" : "disabled";
alert(`Homepage Search removal ${state}. Refresh the page to apply.`);
},
);
}
// Initialize
function init() {
debug("Initializing...");
// Setup menu
setupMenu();
// Process existing content
Declutterer.processAll();
// Watch for new content
ContentObserver.start();
debug("Ready");
}
// Start when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();