Tampermonkey/Greasemonkey script hack for Medium articles – zaps paywalls overlays nags so you can read without the noise. Not affiliated with Medium. Use at your own risk.
// ==UserScript==
// @name fudium
// @namespace https://github.com/ThoriqFathurrozi/
// @version 1.1
// @description Tampermonkey/Greasemonkey script hack for Medium articles – zaps paywalls overlays nags so you can read without the noise. Not affiliated with Medium. Use at your own risk.
// @author frrzyriq
// @match https://medium.com
// @match https://*.medium.com/*
// @match https://*/*
// @icon https://miro.medium.com/v2/5d8de952517e8160e40ef9841c781cdc14a5db313057fa3c3de41c6f5b494b19
// @grant none
// @run-at document-end
// @noframes
// @homepageURL https://github.com/ThoriqFathurrozi/fudium
// @license MIT; https://raw.githubusercontent.com/ThoriqFathurrozi/fudium/refs/heads/main/LICENSE
// ==/UserScript==
(async () => {
'use strict';
const FREEDIUM_URL = 'https://freedium.cfd/';
const BANNER_ID_ARTICLE = 'fudium-article-banner';
const BANNER_ID_PAGE = 'fudium-page-banner';
// Utility function to wait for elements
const waitForElement = async (selector, timeout = 5000, multiple = false) => {
const queryMethod = multiple ? 'querySelectorAll' : 'querySelector';
const existing = document[queryMethod](selector);
if (multiple ? existing.length > 0 : existing) return existing;
return new Promise((resolve, reject) => {
const observer = new MutationObserver(() => {
const element = document[queryMethod](selector);
if (multiple ? element.length > 0 : element) {
observer.disconnect();
clearTimeout(timeoutId);
resolve(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
const timeoutId = setTimeout(() => {
observer.disconnect();
resolve(multiple ? [] : null);
}, timeout);
});
};
// Create banner elements
const createBanner = (link, isPageBanner = false) => {
const banner = document.createElement('a');
banner.href = FREEDIUM_URL + link;
banner.id = isPageBanner ? BANNER_ID_PAGE : BANNER_ID_ARTICLE;
Object.assign(banner.style, {
position: isPageBanner ? 'fixed' : 'absolute',
padding: '10px',
borderRadius: '5px',
color: 'white',
zIndex: '498',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
textDecoration: 'none',
fontSize: '14px',
fontWeight: 'bold',
...(isPageBanner
? { bottom: '50vh', right: '20px' }
: { top: '0', right: '0' }
)
});
banner.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="16" fill="white" style="vertical-align: middle; margin-right: 5px;">
<path d="M320 96c-35.3 0-64 28.7-64 64v64h192c35.3 0 64 28.7 64 64v224c0 35.3-28.7 64-64 64H192c-35.3 0-64-28.7-64-64V288c0-35.3 28.7-64 64-64v-64c0-70.7 57.3-128 128-128 63.5 0 116.1 46.1 126.2 106.7 2.9 17.4-8.8 33.9-26.3 36.9s-33.9-8.8-36.9-26.3c-5-30.2-31.3-53.3-63-53.3m40 328c13.3 0 24-10.7 24-24s-10.7-24-24-24h-80c-13.3 0-24 10.7-24 24s10.7 24 24 24z"/>
</svg>
Open Free
`;
return banner;
};
// Check if article is member-only
const isMemberOnlyArticle = async (element) => {
if (element) {
return element.querySelector('button[aria-label="Member-only story"]') !== null;
}
// Check for page-level paywall indicators
const paywallButton = await waitForElement('#paywallButton-programming', 2000);
if (!paywallButton) {
return false;
}
return [...document.querySelectorAll('div>p')]
.some(p => p.innerText.includes('Member-only story'));
};
// Check if current page is an full article page
const isFullArticlePage = () => {
const locationPath = window.location.pathname;
const exceptPath = ['/@MediumStaff/list'];
if (exceptPath.some(except => locationPath.includes(except))) return false;
const pathParts = locationPath.split('/').filter(Boolean);
if (pathParts.length === 0) return false;
const lastPart = pathParts[pathParts.length - 1];
const possibleHash = lastPart.split('-').pop();
return /^[a-f0-9]{12,}$/i.test(possibleHash);
};
// Check if we're on Medium
const isMediumSite = async () => {
try {
const logo = await waitForElement('#wordmark-medium-desc', 2000);
return logo !== null;
} catch {
return false;
}
};
// Add banners to article cards
const addArticleBanners = async () => {
try {
const articles = await waitForElement('article', 4000, true);
if (!articles.length) return;
for (const article of articles) {
const linkElement = article.querySelector('div[role="link"]');
if (!linkElement || linkElement.querySelector(`#${BANNER_ID_ARTICLE}`)) continue;
if (await isMemberOnlyArticle(linkElement)) {
linkElement.style.position = 'relative';
linkElement.appendChild(createBanner(linkElement.dataset.href));
}
}
} catch (error) {
console.error('Error adding article banners:', error);
}
};
// Add banner to article page
const addPageBanner = async () => {
if (document.querySelector(`#${BANNER_ID_PAGE}`) || !await isMemberOnlyArticle()) {
return;
}
try {
const heading = await waitForElement('h1', 4000);
if (heading?.parentElement) {
heading.parentElement.style.position = 'relative';
heading.parentElement.appendChild(createBanner(window.location.href, true));
}
} catch (error) {
console.error('Error adding page banner:', error);
}
};
// Remove page banner if not on article page
const cleanupPageBanner = () => {
const banner = document.querySelector(`#${BANNER_ID_PAGE}`);
if (banner) banner.remove();
};
// Throttled scroll handler
let scrollTimeout;
const handleScroll = () => {
if (scrollTimeout) return;
scrollTimeout = setTimeout(async () => {
if (isFullArticlePage()) {
await addPageBanner();
} else {
cleanupPageBanner();
await addArticleBanners();
}
scrollTimeout = null;
}, 150);
};
// Initialize the script
const initialize = async () => {
if (!await isMediumSite()) return;
if (isFullArticlePage()) {
await addPageBanner();
} else {
await addArticleBanners();
}
// Set up scroll listener with throttling
window.addEventListener('scroll', handleScroll, { passive: true });
};
// Handle navigation changes (SPA)
const handleNavigation = async () => {
await new Promise(resolve => setTimeout(resolve, 100));
await initialize();
};
// Listen for navigation events
['popstate', 'pushstate', 'locationchange'].forEach(event => {
window.addEventListener(event, handleNavigation);
});
// Start the script
await initialize();
})();