Declutter LinkedIn

Remove news, ads, and other clutter from LinkedIn

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
})();