Reddit Ad Blocker

Blocks sponsored ads, tracking pixels and featured content on Reddit

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Reddit Ad Blocker
// @namespace    reddit-ad-blocker
// @version      2.1
// @description  Blocks sponsored ads, tracking pixels and featured content on Reddit
// @author       jwalley
// @match        https://www.reddit.com/*
// @match        https://old.reddit.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const LOG_PREFIX = '[Reddit Ad Blocker]';
    const DEBUG = false;

    function log(...args) {
        if (DEBUG) console.log(LOG_PREFIX, ...args);
    }

    // ========================================
    // 1. NETWORK INTERCEPTION - Block ad tracking URLs
    // ========================================

    const TRACKING_PATTERNS = [
        'alb.reddit.com/i.gif',          // Ad impression tracking pixels
        'alb.reddit.com/skatepark',       // Ad tracking beacon
        '/shreddit/assets/pix/ads/',      // Ad pixel assets
        '/shreddit/assets/ad/',           // Ad image assets
        'id.rlcdn.com',                   // LiveRamp identity tracking
    ];

    function isBlockedUrl(url) {
        if (typeof url !== 'string') return false;
        return TRACKING_PATTERNS.some(p => url.includes(p));
    }

    // fetch intercept
    const originalFetch = window.fetch;
    window.fetch = function (input, init) {
        const url = typeof input === 'string' ? input : input?.url || '';
        if (isBlockedUrl(url)) {
            log('fetch blocked:', url.substring(0, 80));
            return Promise.resolve(new Response('', { status: 204 }));
        }
        return originalFetch.apply(this, arguments);
    };

    // XMLHttpRequest intercept
    const origXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url, ...rest) {
        if (isBlockedUrl(url)) {
            log('XHR blocked:', url.substring(0, 80));
            this._blocked = true;
        }
        return origXHROpen.call(this, method, url, ...rest);
    };
    const origXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (...args) {
        if (this._blocked) {
            Object.defineProperty(this, 'readyState', { value: 4 });
            Object.defineProperty(this, 'status', { value: 204 });
            this.dispatchEvent(new Event('load'));
            return;
        }
        return origXHRSend.apply(this, args);
    };

    // sendBeacon intercept
    const origBeacon = navigator.sendBeacon?.bind(navigator);
    if (origBeacon) {
        navigator.sendBeacon = function (url, data) {
            if (isBlockedUrl(url)) {
                log('Beacon blocked:', url.substring(0, 80));
                return true;
            }
            return origBeacon(url, data);
        };
    }

    // ========================================
    // 2. CSS STYLES - Directly hide ads
    // ========================================

    const css = document.createElement('style');
    css.id = 'reddit-ad-blocker-css';
    css.textContent = `
        /* === MAIN AD POSTS === */
        /* shreddit-ad-post: Reddit's official ad component */
        shreddit-ad-post {
            display: none !important;
        }
        /* HR separator following an ad post */
        shreddit-ad-post + hr {
            display: none !important;
        }

        /* === AD LINK COMPONENTS === */
        shreddit-dynamic-ad-link {
            pointer-events: none !important;
        }
        shreddit-dynamic-ad-link[aria-label^="Advertisement"] {
            display: none !important;
        }

        /* === SIDEBAR AD PANELS === */
        aside#right-rail-entity-panel-root,
        [aria-label="İşletmede Öne Çıkanlar Paneli"],
        [aria-label="Featured Businesses Panel"],
        [aria-label*="İşletmede Öne Çıkanlar"],
        [aria-label*="Featured Businesses"] {
            display: none !important;
        }

        /* === AD SLOTS === */
        shreddit-ad-slot,
        [data-ad-slot],
        .advert,
        .promotedlink,
        .ad-container {
            display: none !important;
        }

        /* === PREMIUM UPSELL === */
        [data-testid="premium-upsell"],
        [data-testid="premium-banner"],
        .premium-upsell {
            display: none !important;
        }

        /* === OLD REDDIT SUPPORT === */
        .promoted,
        #siteTable_promoted,
        .link.promoted {
            display: none !important;
        }
    `;

    if (document.documentElement) {
        document.documentElement.appendChild(css);
    } else {
        document.addEventListener('DOMContentLoaded', () => {
            document.head.appendChild(css);
        });
    }

    // ========================================
    // 3. MUTATION OBSERVER - Catch dynamic ads
    // ========================================

    let pendingFrame = null;
    let hiddenCount = 0;

    function hideElement(el) {
        if (!el || el.style.display === 'none') return false;
        el.style.setProperty('display', 'none', 'important');
        // Also hide the next sibling HR
        const next = el.nextElementSibling;
        if (next && next.tagName === 'HR') {
            next.style.setProperty('display', 'none', 'important');
        }
        hiddenCount++;
        log(`Ad hidden #${hiddenCount}:`, el.tagName, el.getAttribute('data-code-comment-1') || '');
        return true;
    }

    function scanForAds() {
        let found = 0;

        // Main target: shreddit-ad-post elements
        document.querySelectorAll('shreddit-ad-post').forEach(el => {
            if (hideElement(el)) found++;
        });

        // Ad slots
        document.querySelectorAll('shreddit-ad-slot, [data-ad-slot]').forEach(el => {
            if (hideElement(el)) found++;
        });

        // Sidebar ad panel
        const sidebar = document.querySelector('#right-rail-entity-panel-root');
        if (sidebar && sidebar.style.display !== 'none') {
            sidebar.style.setProperty('display', 'none', 'important');
            found++;
        }

        // "Advertisement:" ile başlayan aria-label'lar
        document.querySelectorAll('[aria-label^="Advertisement:"]').forEach(el => {
            const container = el.closest('shreddit-ad-post') || el.closest('article') || el.parentElement;
            if (container && container.style.display !== 'none') {
                hideElement(container);
                found++;
            }
        });

        // Old Reddit support
        document.querySelectorAll('.link.promoted, .promoted').forEach(el => {
            if (hideElement(el)) found++;
        });

        if (found > 0) log(`Scan: ${found} new ads hidden (total: ${hiddenCount})`);
    }

    function scheduleScan() {
        if (pendingFrame) cancelAnimationFrame(pendingFrame);
        pendingFrame = requestAnimationFrame(() => {
            scanForAds();
            pendingFrame = null;
        });
    }

    function initObserver() {
        const observer = new MutationObserver(mutations => {
            let shouldScan = false;
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType !== Node.ELEMENT_NODE) continue;
                    const tag = node.tagName;
                    if (tag === 'SHREDDIT-AD-POST' ||
                        tag === 'SHREDDIT-DYNAMIC-AD-LINK' ||
                        tag === 'SHREDDIT-AD-SLOT' ||
                        tag === 'ARTICLE' ||
                        tag === 'SHREDDIT-POST' ||
                        node.querySelector?.('shreddit-ad-post, shreddit-ad-slot, shreddit-dynamic-ad-link')) {
                        shouldScan = true;
                        break;
                    }
                }
                if (shouldScan) break;
            }
            if (shouldScan) scheduleScan();
        });

        observer.observe(document.body || document.documentElement, {
            childList: true,
            subtree: true,
        });
        log('MutationObserver started');
    }

    // ========================================
    // 4. INITIALIZATION
    // ========================================

    if (document.body) {
        initObserver();
        scanForAds();
    } else {
        document.addEventListener('DOMContentLoaded', () => {
            initObserver();
            scanForAds();
        });
    }

    // Scroll scanning (infinite scroll)
    let scrollTimer = null;
    window.addEventListener('scroll', () => {
        if (scrollTimer) return;
        scrollTimer = setTimeout(() => {
            scanForAds();
            scrollTimer = null;
        }, 500);
    }, { passive: true });

    // Watch SPA URL changes
    let lastUrl = location.href;
    const urlObs = new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            log('URL changed:', lastUrl);
            setTimeout(scanForAds, 1000);
            setTimeout(scanForAds, 3000);
        }
    });
    document.addEventListener('DOMContentLoaded', () => {
        const title = document.querySelector('title');
        if (title) urlObs.observe(title, { childList: true, subtree: true, characterData: true });
    });

    log('Reddit Ad Blocker v2.1 initialized');

})();