// ==UserScript== // @name Declutter Pinterest // @namespace August4067 // @version 0.9.0 // @description Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly // @license MIT // @match https://www.pinterest.com/* // @match https://*.pinterest.com/* // @match https://*.pinterest.co.uk/* // @match https://*.pinterest.fr/* // @match https://*.pinterest.de/* // @match https://*.pinterest.ca/* // @match https://*.pinterest.jp/* // @match https://*.pinterest.it/* // @match https://*.pinterest.au/* // @icon https://www.pinterest.com/favicon.ico // @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @sandbox Javascript // @downloadURL https://update.greasyfork.icu/scripts/512469/Declutter%20Pinterest.user.js // @updateURL https://update.greasyfork.icu/scripts/512469/Declutter%20Pinterest.meta.js // ==/UserScript== /*--- waitForKeyElements(): A utility function, for Greasemonkey scripts, that detects and handles AJAXed content. Usage example: waitForKeyElements ( "div.comments" , commentCallbackFunction ); //--- Page-specific function to do what we want when the node is found. function commentCallbackFunction (jNode) { jNode.text ("This comment changed by waitForKeyElements()."); } IMPORTANT: This function requires your script to have loaded jQuery. */ // Pulled from: https://gist.github.com/raw/2625891/waitForKeyElements.js function waitForKeyElements( selectorTxt /* Required: The jQuery selector string that specifies the desired element(s). */, actionFunction /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */, bWaitOnce /* Optional: If false, will continue to scan for new elements even after the first match is found. */, iframeSelector /* Optional: If set, identifies the iframe to search. */, ) { var targetNodes, btargetsFound; if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt); else targetNodes = $(iframeSelector).contents().find(selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; /*--- Found target node(s). Go through each and act if they are new. */ targetNodes.each(function () { var jThis = $(this); var alreadyFound = jThis.data("alreadyFound") || false; if (!alreadyFound) { //--- Call the payload function. var cancelFound = actionFunction(jThis); if (cancelFound) btargetsFound = false; else jThis.data("alreadyFound", true); } }); } else { btargetsFound = false; } //--- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace(/[^\w]/g, "_"); var timeControl = controlObj[controlKey]; //--- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { //--- The only condition where we need to clear the timer. clearInterval(timeControl); delete controlObj[controlKey]; } else { //--- Set a timer, if needed. if (!timeControl) { timeControl = setInterval(function () { waitForKeyElements( selectorTxt, actionFunction, bWaitOnce, iframeSelector, ); }, 300); controlObj[controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; } // We will set the Pinterest page title to this, to remove // the flashing title notifications like Pinterest (2) const ORIGINAL_TITLE = 'Pinterest'; const SETTINGS_CONFIG = { removeShoppablePins: { displayName: "Remove shoppable pins", default: true }, removePinFooters: { displayName: "Remove pin footers", default: false }, removeComments: { displayName: "Remove pin comments", default: false }, removeNavbarMessagesIcon: { displayName: "Remove navbar messages icon", default: false }, removeNavbarNotificationIcon: { displayName: "Remove navbar notifications icon", default: false } }; class Setting { constructor(name, config) { this.name = name; this.displayName = config.displayName; this.default = config.default; } currentValue() { return GM_getValue(this.name, this.default); } toggleSetting() { GM_setValue(this.name, !this.currentValue()); } } // Create settings object by mapping config to Setting instances const SETTINGS = Object.fromEntries( Object.entries(SETTINGS_CONFIG).map(([name, config]) => [ name, new Setting(name, config) ]) ); // MENU SETTINGS function toggleMenuSetting(settingName) { var setting = SETTINGS[settingName]; setting.toggleSetting(); updateSettingsMenu(); console.debug(`Setting ${settingName} set to: ${setting.currentValue()}}`); location.reload(); } function updateSettingsMenu() { for (const [setting_name, setting] of Object.entries(SETTINGS)) { GM_registerMenuCommand( `${setting.displayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`, () => { toggleMenuSetting(setting_name); }, ); } } // HELPER FUNCTIONS function waitAndRemove(selector, removeFunction) { if (removeFunction == undefined) { removeFunction = (elem) => elem.remove(); } waitForKeyElements(selector, function (node) { if (node && node.length > 0) { removeFunction(node[0]); } }); } /** * Hide an element by setting its display style to "none" if it exists * @param {HTMLElement} element - The DOM element to hide */ function hideElement(element) { if (element) { element.style.display = "none"; } } /** * Return an array of pins, filtering out those that already have * style.display == "none" (pins that we have removed) * @param {*} pins * @returns array of pins */ function filterRemovedPins(pins) { return pins ? pins.filter((pin) => pin.style.display !== "none") : []; } // DECLUTTER BOARDS /** * Clean the Shop button from the top of boards (from the 3 button group with "Shop", "Organize", and "More ideas") */ function cleanShopButtonsFromBoard() { console.debug("Cleaning Shop buttons from board"); waitForKeyElements('div[data-test-id="board-tools"]', function (node) { var shopButton = node[0].querySelector('div[data-test-id="Shop"]'); hideElement(shopButton); console.debug("Removed Shop button from top of board"); }); } /** * Remove the banner of shopping pins wherever we find it (with title "Shop products inspired by this board"). * They sometimes show up at the top of boards, at the bottom of boards, and at the top of searches. */ function cleanShopByBanners() { waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) { var shopByBannerAtTopOfBoard = node[0].closest( 'div[class="PKX zI7 iyn Hsu"]', ); hideElement(shopByBannerAtTopOfBoard); console.debug("Removed shop by banner from top of board"); if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) { var shopByBannerAtBottomOfBoard = node[0].closest( 'div[class="gcw zI7 iyn Hsu"]', ); hideElement(shopByBannerAtBottomOfBoard); console.debug("Removed shop by banner from bottom of board"); } var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]'); hideElement(shopByBannerAtTopOfSearch); console.debug("Removed shop by banner from top of search results"); }); } // DECLUTTER NAVBAR /** * The search bar now has dynamic placeholder text with suggested searches. These are distracting, and we will remove them. */ function cleanSearchBarDynamicText(searchBox) { searchBox .querySelector('div[data-test-id="dynamic-search-placeholder"]') ?.remove(); } function cleanSearchBarSuggestions(searchBox) { var suggestionsMenu = searchBox.querySelector('div[id="SuggestionsMenu"]'); if (!suggestionsMenu) { return; } var popularOnPinterestSuggestions = suggestionsMenu.querySelector( 'div[title="Popular on Pinterest"]', ); if (popularOnPinterestSuggestions) { var popularOnPinterestBanner = popularOnPinterestSuggestions.closest( 'div[class="jzS un8 L4V jDD"]', ); hideElement(popularOnPinterestBanner); console.debug("Removed Popular on Pinterest search suggestions"); } } /** * A series of tabs is at the top of search results to refine results by your interests ("all", "holiday finds", "my board 1", "my board 2", etc). * We will remove the shopping / promoted tabs */ function cleanNavTabCarousel() { waitForKeyElements('div[class="localNavTabCarousel"]', function (nodes) { var navTabCarousel = nodes[0]; if (!navTabCarousel) { return; } const promotedTabs = new Set(["holiday finds"]); navTabCarousel.querySelectorAll('div[class="xuA"]').forEach((item) => { if (!item.innerText) { return; } var navTabText = item.innerText.trim().toLowerCase(); if (promotedTabs.has(navTabText)) { hideElement(item); console.debug(`Hid promoted nav tab: ${navTabText}`); } }); }); } function removeBellIcon() { if (SETTINGS.removeNavbarNotificationIcon.currentValue()) { const bellIconDiv = document.querySelector('div[data-test-id="bell-icon"]'); hideElement(bellIconDiv); console.debug("Removed bell icon from navbar."); } } function removeMessagesIcon() { if (SETTINGS.removeNavbarMessagesIcon.currentValue()) { const messagesIconDiv = document.querySelector( 'div[aria-label="Messages"]', ); if (messagesIconDiv) { var messagesParent = messagesIconDiv.closest( 'div[class="XiG zI7 iyn Hsu"]', ); hideElement(messagesParent); console.debug("Removed messages button"); } } } function removeExploreTabNotificationsIcon() { console.debug("Removing notifications icon from Explore tab (top nav and sidebar)"); // --- Remove notification icon from Explore tab in the top nav (old behavior) var exploreTab = document.querySelector('div[data-test-id="today-tab"]'); if (exploreTab) { var notificationsIcon = exploreTab.querySelector( 'div[aria-label="Notifications"]', ); hideElement(notificationsIcon); if (notificationsIcon) { console.debug("Removed notifications icon from Explore tab (top nav)"); } } // --- Remove notification badge from Explore tab in the sidebar (new behavior) // Find the Explore tab link in the sidebar var exploreTabLink = document.querySelector('a[data-test-id="today-tab"]'); if (exploreTabLink) { // The parent of the link is the icon container, its parent is the sidebar item var iconContainer = exploreTabLink.closest('div[class*="XiG"]'); var sidebarItem = iconContainer?.parentElement?.parentElement; if (sidebarItem) { // The notification badge is a sibling div with class "MIw" and pointer-events: none var notificationBadge = sidebarItem.parentElement?.querySelector('.MIw[style*="pointer-events: none"]'); if (notificationBadge) { hideElement(notificationBadge); console.debug("Removed notifications badge from Explore tab (sidebar)"); } } } } function cleanNavBarCallback(mutationsList) { console.debug("Cleaning navbar"); removeBellIcon(); removeMessagesIcon(); var callbackCount = 0; waitForKeyElements('div[data-test-id="today-tab"]', function (nodes) { new MutationObserver((mutations, observer) => { removeExploreTabNotificationsIcon(); }).observe(nodes[0], { childList: true, subtree: true, }); }); removeExploreTabNotificationsIcon(); waitForKeyElements('div[id="searchBoxContainer"]', function (nodes) { new MutationObserver((mutations, observer) => { cleanSearchBarSuggestions(nodes[0]); cleanSearchBarDynamicText(nodes[0]); }).observe(nodes[0], { childList: true, subtree: true, }); }); } function cleanVerticalNavBar(verticalNavContent) { // TODO: Remove notification badges from explore icon waitForKeyElements('div[aria-label="Messages"]', function (nodes) { if (SETTINGS.removeNavbarMessagesIcon.currentValue()) { if (nodes && nodes.length > 0) { var closest = nodes[0].closest('div[class="xuA"]'); hideElement(closest); } } }); waitForKeyElements('div[aria-label="Notifications"]', function (nodes) { if (SETTINGS.removeNavbarNotificationIcon.currentValue()) { if (nodes && nodes.length > 0) { var closest = nodes[0].closest('div[class="xuA"]'); hideElement(closest); } } }); } function cleanNavBar() { // This is the nav bar at the top of the page waitForKeyElements('div[id="HeaderContent"]', function (nodes) { const headerContent = document.getElementById("HeaderContent"); if (headerContent) { var observerCallCount = 0; const observer = new MutationObserver((mutations, observer) => { cleanNavBarCallback(mutations); if (++observerCallCount >= 5) { observer.disconnect(); console.debug("Disconnected cleanNavBar() mutation observer"); } }); observer.observe(headerContent, { childList: true, subtree: true, }); cleanNavBarCallback([]); } }); // This is the vertical nav bar at the left of the page waitForKeyElements('nav[id="VerticalNavContent"]', function (nodes) { console.debug("Cleaning vertical nav bar"); if (nodes && nodes.length > 0) { console.debug("Cleaning vertical nav bar:", nodes[0]); cleanVerticalNavBar(nodes[0]); } }); } // DECLUTTER PINS function cleanShoppingAds(pins) { const shoppingAdDivs = document.querySelectorAll( "div.Ch2.zDA.IZT.CKL.tBJ.dyH.iFc.GTB.H2s", ); shoppingAdDivs.forEach((adDiv) => { let parent = adDiv.closest('div[role="listitem"]'); hideElement(parent); console.debug("Removed shopping container"); }); } function cleanIdeasYouMightLove(pins) { pins.forEach((pin) => { if ( pin.textContent.toLowerCase().includes("ideas you might love") || pin.textContent.toLowerCase().includes("shop similar") || pin.textContent.toLowerCase().includes("shop featured boards") ) { hideElement(pin); console.debug('Removed "Ideas you might love" item:', pin); } }); } function removePinFooters(pins) { if (!SETTINGS.removePinFooters.currentValue()) { return; } pins.forEach((pin) => { const footer = pin.querySelector('div[data-test-id="pinrep-footer"]'); if (!footer) { return; } hideElement(footer); console.debug("Removed pin footer:", footer); }); } /** * Remove Shoppable Pins by looking for a little tag ("Shoppable Pin indicator") in the pin somewhere. * Shoppable pin indicators could be in the footer, or as an svg on top of the image */ function removeShoppablePins(pins) { if (!SETTINGS.removeShoppablePins.currentValue()) { return; } pins.forEach((pin) => { const shoppableIndicator = pin.querySelector( '[aria-label="Shoppable Pin indicator"]', ); if (shoppableIndicator) { hideElement(pin); console.debug("Removed shoppable pin:", pin); } }); } function removeSponsoredPins(pins, mutations) { var promotedPinSelector = 'a[aria-label="Promoted by"], a[aria-label="Promoted by; Opens a new tab"], div[title="Sponsored"]'; pins.forEach((pin) => { if (pin && pin.querySelector(promotedPinSelector)) { hideElement(pin); } }); } /** * Pinterest now has a "Shop now" module as the first result of some searches. * We will remove this. */ function removeShoppingModule(pins) { console.debug("Cleaning shopping modules"); if (pins) { pins.forEach((pin) => { var module = pin.querySelector("div.Module"); if (!module || !module.innerText) { return; } const shoppingModuleTexts = new Set(["shop now", "continue shopping"]); const innerTextLines = module.innerText .trim() .split("\n") .map((x) => x.trim().toLowerCase()); for (var i = 0; i < innerTextLines.length; i++) { if (shoppingModuleTexts.has(innerTextLines[i])) { hideElement(pin); console.debug("Removed shopping module"); break; } } }); } } /** * Pinterest now is promoting their own shopping boards (from the "Pinterest Shop" user). We * will remove those. */ function removeFeaturedBoards(pins) { if (pins) { pins.forEach((pin) => { if (pin.style.display === "none") { return; } if (pin.querySelector('[data-test-id="pinRepPresentation"]')) { return; } var innerText = pin.innerText ? pin.innerText.trim().toLowerCase() : ""; var splitText = innerText.split("\n").map((x) => x.trim().toLowerCase()); if ( splitText.includes("explore featured boards") || splitText.includes("pinterest shop") ) { hideElement(pin); console.debug("Removed featured boards module"); } }); } } function cleanRelatedPinsSection(mutations) { console.debug("Cleaning related pins"); var pins = document.querySelectorAll('div[role="listitem"]'); cleanIdeasYouMightLove(pins); cleanShoppingAds(pins); removePinFooters(pins); removeShoppablePins(pins); removeSponsoredPins(pins, mutations); removeShoppingModule(pins); removeFeaturedBoards(pins); } function cleanProductListingPinPage() { waitAndRemove('div[data-test-id="product-price"]'); waitAndRemove('div[data-test-id="pdp-product-metadata-domain-owner"]'); waitAndRemove( 'div[data-test-id="product-shop-button"]', (elem) => (elem.parentElement.style.display = "none"), ); waitAndRemove( 'div[data-test-id="product-description"]', (elem) => (elem.parentElement.style.display = "none"), ); } function cleanPinVisualContainer() { var pinVisualContainer = document.querySelector( 'div[data-test-id="closeup-visual-container"]', ); if (pinVisualContainer) { var shopButton = pinVisualContainer.querySelector( 'div[data-test-id="experimental-closeup-image-overlay-layer-shop-button"]', ); hideElement(shopButton); var domainLinkButton = pinVisualContainer.querySelector( 'div[data-test-id="experimental-closeup-image-overlay-layer-domain-link-button"]' ); hideElement(domainLinkButton); var closeupImageOverlay = pinVisualContainer.querySelector( 'div[data-test-id="closeup-image-overlay-layer-domain-link-button"]' ); hideElement(closeupImageOverlay); } } /** * The collapsible layout seems to be only used for the "Shop the look" section. * So we will remove it. */ function removeShopTheLookSection() { // 1. CSS rule as a backup (will not match text, but can help if Pinterest adds a class or data attribute in the future) const style = document.createElement('style'); style.textContent = ` /* This will hide the whole section if you can add a class or data attribute in the future */ /* For now, we rely on JS for text matching */ `; document.head.appendChild(style); // 2. Function to hide all "Shop the look" sections function hideShopTheLookSection() { document.querySelectorAll('div[data-test-id="collapsible-layout"]').forEach(function(layout) { // Look for any h2 or h3 with text "Shop the look" (case-insensitive) const headings = layout.querySelectorAll('h2, h3'); for (const heading of headings) { if (heading.textContent && heading.textContent.trim().toLowerCase() === 'shop the look') { hideElement(layout); console.debug('Removed Shop the look section (robust observer)'); break; } } }); } // Initial hide hideShopTheLookSection(); // 3. Observe the whole body for new nodes const observer = new MutationObserver(hideShopTheLookSection); observer.observe(document.querySelector('div[role="list"]'), { childList: true, subtree: true }); } function cleanComments() { if (SETTINGS.removeComments.currentValue()) { waitForKeyElements("#canonical-card", function (canonicalCards) { if (canonicalCards && canonicalCards.length == 1) { var canonicalCard = canonicalCards[0]; if (!canonicalCard) { return; } var hasCommentsHeading = canonicalCard.querySelector("#comments-heading") != null; if (hasCommentsHeading) { hideElement(canonicalCard); console.debug("Removed comments section from pin"); } } }); waitForKeyElements( 'div[data-test-id="inline-comment-composer-container"]', function (nodes) { var commentBox = nodes[0]; if (commentBox) { hideElement(commentBox); console.debug("Removed comment box from pin"); } }, ); } } function cleanPinDescriptionContainer() { var pinDescriptionContainer = document.querySelector( 'div[data-test-id="description-content-container"]', ); if (pinDescriptionContainer) { var shopButton = pinDescriptionContainer.querySelector( 'div[data-test-id="product-shop-button"]', ); hideElement(shopButton); // Remove the new Shop the look section var collapsibleLayouts = document.querySelectorAll('div[data-test-id="collapsible-layout"]'); collapsibleLayouts.forEach(function(layout) { var heading = layout.querySelector('h2#comments-heading'); if (heading && heading.textContent.trim().toLowerCase() === 'shop the look') { hideElement(layout); console.debug('Removed Shop the look section'); } }); } } /** * Pinterest adds the number of notifications to the title * in a distracting, flashing manner like Pinerest (2). * So this will keep the page title at: Pinterest */ function enforceTitle() { if (document.title !== ORIGINAL_TITLE) { console.debug( `Changing title from: ${document.title} to ${ORIGINAL_TITLE}`, ); document.title = ORIGINAL_TITLE; } } /** * Pinterest now has a popup modal asking you to disable adblock, if it is present. * * We will remove this modal. */ function removeAntiAdblockModalIfExists() { console.debug("Waiting for anti-Adblock modal"); waitForKeyElements('div[aria-label="Ad blocker modal"]', function (nodes) { nodes[0]?.remove(); document.querySelector('div[name="trap-focus"]')?.remove(); document.body.style.overflow = "auto"; console.debug("Removed anti-Adblock modal"); }); } // PAGE IDENTIFIERS function isProductPin() { return document.getElementById("product-sticky-container ") != null; } function hasDescriptionContentContainer() { return ( document.querySelector( 'div[data-test-id="description-content-container"]', ) != null ); } function isBoard() { return document.querySelector('div[data-test-id="board-header"]') != null; } function observeSidebarForExploreBadge() { // Find the sidebar navigation container (adjust selector if needed) const sidebarNav = document.querySelector('nav[id="VerticalNavContent"]') || document.querySelector('div[role="navigation"]'); if (!sidebarNav) { // Try again later if sidebar not yet loaded setTimeout(observeSidebarForExploreBadge, 500); return; } // Remove any existing badge immediately removeExploreTabNotificationsIcon(); // Set up observer const observer = new MutationObserver(() => { removeExploreTabNotificationsIcon(); }); observer.observe(sidebarNav, { childList: true, subtree: true }); } function main() { "use strict"; console.debug("Running main()"); updateSettingsMenu(); cleanNavBar(); cleanComments(); cleanNavTabCarousel(); removeAntiAdblockModalIfExists(); if (isProductPin()) { cleanProductListingPinPage(); } cleanShopButtonsFromBoard(); cleanShopByBanners(); // The nav tab tab carousel pops up after page load waitForKeyElements('div[class="localNavTabCarousel"]', function (nodes) { cleanNavTabCarousel(); }); cleanPinDescriptionContainer(); removeShopTheLookSection(); cleanPinVisualContainer(); // The pin visual container changes to add those little "shop" buttons // over the top of the image, so we need to watch for those and remove them waitForKeyElements( 'div[data-test-id="closeup-visual-container"]', function (nodes) { new MutationObserver((mutations, observer) => { cleanPinVisualContainer(); }).observe(nodes[0], { childList: true, subtree: true, }); }, ); const relatedPinsSectionCleanerObserver = new MutationObserver( (mutations) => { cleanRelatedPinsSection(mutations); }, ); waitForKeyElements('div[role="list"]', function (node) { // Related pins section must be watched for changes, // as new pins pop up as the user scrolls cleanRelatedPinsSection([]); relatedPinsSectionCleanerObserver.observe(node[0], { childList: true, }); }); enforceTitle(); const titleElement = document.querySelector("title"); new MutationObserver(enforceTitle).observe(titleElement, { childList: true }); observeSidebarForExploreBadge(); } main(); let lastUrl = window.location.href; setInterval(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { console.debug( `Detected new page, currentURL=${currentUrl}, previousURL=${lastUrl}`, ); lastUrl = currentUrl; main(); } }, 750);