Remove news, ads, and other clutter from LinkedIn
// ==UserScript==
// @name Declutter LinkedIn
// @namespace August4067
// @version 2.0.0
// @description Remove news, ads, and other clutter from LinkedIn
// @author August4067
// @license MIT
// @match https://www.linkedin.com/*
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @icon https://www.linkedin.com/favicon.ico
// ==/UserScript==
/* jshint esversion: 8 */
/* eslint-env es2017 */
(function () {
"use strict";
// ============================================
// CONFIGURATION
// ============================================
const CONFIG = {
pollInterval: 2000,
throttleDelay: 100,
debug: false,
};
const SETTINGS_CONFIG = {
removeNews: {
displayName: "Remove LinkedIn News",
default: true,
},
removePremiumUpsells: {
displayName: "Remove Premium upsells",
default: true,
},
removePromotedPosts: {
displayName: "Remove promoted posts",
default: true,
},
removeGames: {
displayName: "Remove games/puzzles",
default: true,
},
};
// ============================================
// SETTINGS
// ============================================
class Setting {
constructor(name, config) {
this.name = name;
this.displayName = config.displayName;
this.default = config.default;
}
get value() {
return GM_getValue(this.name, this.default);
}
set value(val) {
GM_setValue(this.name, val);
}
toggle() {
this.value = !this.value;
}
}
const Settings = Object.fromEntries(
Object.entries(SETTINGS_CONFIG).map(([name, config]) => [
name,
new Setting(name, config),
])
);
// ============================================
// UTILITIES
// ============================================
function debug(message, ...args) {
if (CONFIG.debug) {
console.log(`[Declutter LinkedIn] ${message}`, ...args);
}
}
// ============================================
// CSS INJECTION
// ============================================
function injectStyles() {
if (document.getElementById("declutter-linkedin-styles")) return;
const style = document.createElement("style");
style.id = "declutter-linkedin-styles";
style.textContent = `
/* Hide premium upsell links in menus instantly via CSS */
a[href*="/premium/products/"],
li:has(> a[href*="/premium/products/"]),
[data-view-name="seeker-next-best-action-card"],
[data-view-name="premium-upsell-link"] {
display: none !important;
}
/* Hide games/puzzles links instantly via CSS */
${GM_getValue("removeGames", true) ? `a[href*="/games/"] { display: none !important; }` : ""}
`;
(document.head || document.documentElement).appendChild(style);
}
// Inject as early as possible
injectStyles();
// ============================================
// MAIN LOGIC
// ============================================
const Declutterer = {
// Walk up from an element to find the outermost card container.
// LinkedIn wraps upsells in nested divs — we need to hide the outermost
// one to avoid blank boxes.
_findCard(el) {
// Walk up through componentkey ancestors, then check if there's a
// non-componentkey wrapper above that (the visual card div)
let card = el.closest('[componentkey]');
if (!card) return null;
// Keep walking up through componentkey parents
let parent = card.parentElement?.closest('[componentkey]');
while (parent) {
card = parent;
parent = card.parentElement?.closest('[componentkey]');
}
// Check if the card's grandparent is the visual card wrapper
// (div without componentkey that acts as the card boundary)
let wrapper = card.parentElement;
while (wrapper && wrapper !== document.body) {
// Stop at elements that contain other sibling content (not just wrappers)
if (wrapper.children.length > 1) break;
wrapper = wrapper.parentElement;
}
// Use the last single-child wrapper, or fall back to the componentkey card
let target = card;
let cur = card.parentElement;
while (cur && cur !== document.body && cur.children.length === 1) {
target = cur;
cur = cur.parentElement;
}
return target;
},
removeNews() {
if (!Settings.removeNews.value) return;
// Find "LinkedIn News" heading and hide its componentkey container
// (not the whole card, since games/puzzles are siblings)
document.querySelectorAll("p").forEach((p) => {
if (p.dataset.dlHidden) return;
if (p.textContent.trim() === "LinkedIn News") {
const container = p.closest('[componentkey]');
if (container && !container.dataset.dlHidden) {
container.style.setProperty("display", "none", "important");
container.dataset.dlHidden = "1";
debug("Hidden LinkedIn News section");
}
}
});
// Also hide news links in case they're outside the componentkey
document.querySelectorAll('a[href*="/news/story/"]').forEach((a) => {
if (!a.dataset.dlHidden) {
a.style.setProperty("display", "none", "important");
a.dataset.dlHidden = "1";
}
});
// Hide "Top stories" label
document.querySelectorAll("p").forEach((p) => {
if (p.dataset.dlHidden) return;
if (p.textContent.trim() === "Top stories") {
p.style.setProperty("display", "none", "important");
p.dataset.dlHidden = "1";
}
});
// Hide "Show more" button in news section
document.querySelectorAll("button").forEach((btn) => {
if (btn.dataset.dlHidden) return;
if (btn.textContent.trim() === "Show more" &&
btn.closest('[componentkey]')?.querySelector('a[href*="/news/story/"]')) {
btn.style.setProperty("display", "none", "important");
btn.dataset.dlHidden = "1";
}
});
},
removeGames() {
if (!Settings.removeGames.value) return;
// Hide "Today's puzzles" heading and its sibling game links
document.querySelectorAll("p").forEach((p) => {
if (p.dataset.dlHidden) return;
if (p.textContent.trim() === "Today\u2019s puzzles" ||
p.textContent.trim() === "Today's puzzles") {
// Hide the parent container that holds the heading + game link
const container = p.parentElement;
if (container && !container.dataset.dlHidden) {
container.style.setProperty("display", "none", "important");
container.dataset.dlHidden = "1";
debug("Hidden games/puzzles section");
}
}
});
// Hide any remaining game links the CSS might miss
document.querySelectorAll('a[href*="/games/"]').forEach((a) => {
if (!a.dataset.dlHidden) {
a.style.setProperty("display", "none", "important");
a.dataset.dlHidden = "1";
}
});
},
removePromotedPosts() {
if (!Settings.removePromotedPosts.value) return;
// Find <p> elements with "Promoted" text inside feed posts
document.querySelectorAll("p").forEach((p) => {
if (p.dataset.dlHidden) return;
if (p.textContent.trim() === "Promoted") {
// Walk up to the feed post container (role="listitem" only)
// Avoid the componentkey fallback — "Promoted" on /jobs is a label, not an ad
const post = p.closest('[role="listitem"]');
if (post && !post.dataset.dlHidden) {
post.style.setProperty("display", "none", "important");
post.dataset.dlHidden = "1";
debug("Hidden promoted post");
}
}
});
},
// Find the narrowest meaningful container for a premium upsell element.
// Prefers carousel <li>, role="listitem", or immediate componentkey div
// over _findCard which walks too far up and hides entire sections.
_findPremiumContainer(el) {
// In carousels, the upsell is an <li> alongside real profile cards
const carouselItem = el.closest('li[data-testid="carousel-child-container"]');
if (carouselItem) return carouselItem;
// In list layouts, the upsell may be a listitem
const listItem = el.closest('[role="listitem"]');
if (listItem) return listItem;
// Fall back to the immediate componentkey container
const component = el.closest('[componentkey]');
if (component) return component;
// Last resort: use _findCard (home page standalone cards)
return this._findCard(el);
},
removePremiumUpsells() {
if (!Settings.removePremiumUpsells.value) return;
// Hide sections containing the Premium wordmark badge (upsell cards)
document.querySelectorAll('svg#premium-badge-v2-xsmall').forEach((svg) => {
const container = svg.closest('section') || svg.closest('[componentkey]');
if (container && !container.dataset.dlHidden) {
container.style.setProperty("display", "none", "important");
container.dataset.dlHidden = "1";
debug("Hidden Premium upsell (badge section)");
}
});
// Hide links to /premium/products/ and their containers
document.querySelectorAll('a[href*="/premium/products/"]').forEach((a) => {
const card = this._findPremiumContainer(a);
if (card && !card.dataset.dlHidden) {
card.style.setProperty("display", "none", "important");
card.dataset.dlHidden = "1";
debug("Hidden Premium upsell (link)");
}
});
// Hide elements with "Try Premium" / "Try Recruiter" text as a fallback
document.querySelectorAll("p").forEach((p) => {
if (p.dataset.dlHidden) return;
const text = p.textContent.trim();
if (/^try (premium|recruiter)/i.test(text) || /^unlock all with premium/i.test(text) || /^claim premium/i.test(text)) {
const card = this._findPremiumContainer(p);
if (card && !card.dataset.dlHidden) {
card.style.setProperty("display", "none", "important");
card.dataset.dlHidden = "1";
debug("Hidden Premium upsell (text)");
}
}
});
},
};
// ============================================
// PROCESSING
// ============================================
function processPage() {
try {
debug("Processing page");
Declutterer.removeNews();
Declutterer.removeGames();
Declutterer.removePromotedPosts();
Declutterer.removePremiumUpsells();
} catch (error) {
debug("Error during processing:", error);
}
}
// ============================================
// MENU
// ============================================
function setupMenu() {
for (const [key, setting] of Object.entries(Settings)) {
GM_registerMenuCommand(
`${setting.value ? "\u2713" : "\u2717"} ${setting.displayName}`,
() => {
setting.toggle();
const state = setting.value ? "enabled" : "disabled";
alert(`${setting.displayName} ${state}. Refresh the page to apply.`);
}
);
}
}
// ============================================
// INITIALIZATION
// ============================================
function init() {
debug("Initializing...");
setupMenu();
setupMutationObserver();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", processPage);
} else {
processPage();
}
debug("Ready");
}
function setupMutationObserver() {
let timeoutId = null;
const observer = new MutationObserver((mutations) => {
let shouldProcess = false;
for (const m of mutations) {
if (m.addedNodes.length > 0) {
shouldProcess = true;
break;
}
}
if (shouldProcess) {
if (!timeoutId) {
timeoutId = setTimeout(() => {
processPage();
timeoutId = null;
}, CONFIG.throttleDelay);
}
}
});
const target = document.documentElement || document.body;
if (target) {
observer.observe(target, { childList: true, subtree: true });
debug("MutationObserver setup");
}
}
function safeInit() {
try {
init();
} catch (error) {
console.error("[Declutter LinkedIn] Initialization failed:", error);
}
}
safeInit();
// Continuous polling for dynamic content (SPA)
let lastUrl = location.href;
let pollTimer = null;
function startPolling(interval) {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(() => {
processPage();
if (location.href !== lastUrl) {
debug(`Navigation detected: ${lastUrl} -> ${location.href}`);
lastUrl = location.href;
}
}, interval);
}
document.addEventListener("visibilitychange", () => {
startPolling(document.hidden ? CONFIG.pollInterval * 4 : CONFIG.pollInterval);
});
startPolling(CONFIG.pollInterval);
})();