Skip to content

Instantly share code, notes, and snippets.

@abear1538
Created April 3, 2026 18:20
Show Gist options
  • Select an option

  • Save abear1538/eb801c21dbf33130667dc91c1a5ac835 to your computer and use it in GitHub Desktop.

Select an option

Save abear1538/eb801c21dbf33130667dc91c1a5ac835 to your computer and use it in GitHub Desktop.
Nextdoor Cleanup Userscriptt
// ==UserScript==
// @name Nextdoor - Enhanced Feed Filter
// @namespace https://greasyfork.org/users/majik
// @version 2.1.0
// @description Hides ads, sponsored content, and specific keywords/authors from the Nextdoor feed. Adds a quick "Hide" button to posts and a graphical settings panel.
// @author Gemini / Cleanup: majik
// @match https://nextdoor.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// --- Configuration Defaults ---
const DEFAULT_SETTINGS = {
hidePaidAds: true,
hidePromos: true,
hideSponsored: true,
hideNewNeighbors: true,
hideNonFreeClassifieds: true,
addHideLink: true,
hideTaggedInterests: ['dogs', 'cats', 'yoga', 'animal adoption'],
hideKeyPhrases: [
'dog lost', 'dog missing', 'dog found', 'dog loose',
'cat lost', 'cat missing', 'cat found',
'coyote', 'handyman', 'plumber', 'roommate', 'electrician'
],
hideAuthors: ['The San Diego Union-Tribune']
};
// Load Settings
let settings = { ...DEFAULT_SETTINGS, ...GM_getValue('nd_settings', {}) };
// --- CSS Injection ---
const style = document.createElement('style');
style.innerHTML = `
.gm-hide-btn {
color: #6d6e71;
font-size: 13px;
cursor: pointer;
margin-left: 10px;
text-decoration: underline;
}
.gm-hide-btn:hover { color: #000; }
.gm-hidden-post { display: none !important; }
/* Settings Panel Styles */
#gm-settings-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 99999;
width: 450px;
max-height: 80vh;
overflow-y: auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: none;
}
#gm-settings-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 99998;
display: none;
}
.gm-settings-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
display: flex;
justify-content: space-between;
}
.gm-settings-section { margin-bottom: 15px; }
.gm-settings-section label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }
.gm-settings-textarea {
width: 100%;
height: 80px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
font-size: 12px;
resize: vertical;
}
.gm-settings-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.gm-btn {
padding: 8px 16px;
border-radius: 20px;
border: none;
cursor: pointer;
font-weight: 600;
}
.gm-btn-save { background: #2e8231; color: white; }
.gm-btn-cancel { background: #eee; color: #333; }
`;
document.head.appendChild(style);
// --- Helpers ---
const saveSettings = (newSettings) => {
settings = newSettings;
GM_setValue('nd_settings', settings);
};
const containsPhrases = (text, phrases) => {
const lowerText = text.toLowerCase();
return phrases.some(phrase => {
if (Array.isArray(phrase)) {
return phrase.every(word => lowerText.includes(word.toLowerCase()));
}
return lowerText.includes(phrase.toLowerCase());
});
};
// --- Settings UI Logic ---
const createSettingsPanel = () => {
const overlay = document.createElement('div');
overlay.id = 'gm-settings-overlay';
const panel = document.createElement('div');
panel.id = 'gm-settings-panel';
panel.innerHTML = `
<div class="gm-settings-header">
<span>Filter Settings</span>
<span style="cursor:pointer" id="gm-close-x">&times;</span>
</div>
<div class="gm-settings-section">
<label><input type="checkbox" id="gm-hideSponsored" ${settings.hideSponsored ? 'checked' : ''}> Hide Sponsored Ads</label>
</div>
<div class="gm-settings-section">
<label><input type="checkbox" id="gm-hideNonFreeClassifieds" ${settings.hideNonFreeClassifieds ? 'checked' : ''}> Hide Non-Free Classifieds</label>
</div>
<div class="gm-settings-section">
<label><input type="checkbox" id="gm-addHideLink" ${settings.addHideLink ? 'checked' : ''}> Show "Quick Hide" on posts</label>
</div>
<div class="gm-settings-section">
<div style="font-size: 13px; font-weight: 600;">Keywords to Hide (one per line):</div>
<textarea id="gm-hideKeyPhrases" class="gm-settings-textarea">${settings.hideKeyPhrases.join('\n')}</textarea>
</div>
<div class="gm-settings-section">
<div style="font-size: 13px; font-weight: 600;">Authors to Hide (one per line):</div>
<textarea id="gm-hideAuthors" class="gm-settings-textarea">${settings.hideAuthors.join('\n')}</textarea>
</div>
<div class="gm-settings-footer">
<button class="gm-btn gm-btn-cancel" id="gm-btn-cancel">Cancel</button>
<button class="gm-btn gm-btn-save" id="gm-btn-save">Save & Reload</button>
</div>
`;
document.body.appendChild(overlay);
document.body.appendChild(panel);
const close = () => {
panel.style.display = 'none';
overlay.style.display = 'none';
};
document.getElementById('gm-btn-cancel').onclick = close;
document.getElementById('gm-close-x').onclick = close;
document.getElementById('gm-btn-save').onclick = () => {
const updated = {
...settings,
hideSponsored: document.getElementById('gm-hideSponsored').checked,
hideNonFreeClassifieds: document.getElementById('gm-hideNonFreeClassifieds').checked,
addHideLink: document.getElementById('gm-addHideLink').checked,
hideKeyPhrases: document.getElementById('gm-hideKeyPhrases').value.split('\n').filter(s => s.trim()),
hideAuthors: document.getElementById('gm-hideAuthors').value.split('\n').filter(s => s.trim())
};
saveSettings(updated);
location.reload();
};
};
const showSettings = () => {
let panel = document.getElementById('gm-settings-panel');
if (!panel) {
createSettingsPanel();
panel = document.getElementById('gm-settings-panel');
}
panel.style.display = 'block';
document.getElementById('gm-settings-overlay').style.display = 'block';
};
// --- Core Logic ---
const processPost = (post) => {
if (post.dataset.gmProcessed) return;
post.dataset.gmProcessed = "true";
const postText = post.innerText || "";
let shouldHide = false;
let hideReason = "";
// 1. Sponsored / Ad Detection
const isSponsored = postText.includes('Sponsored') ||
post.querySelector('[class*="sponsored"]') ||
post.querySelector('svg[aria-label="Sponsored"]');
if (settings.hideSponsored && isSponsored) {
shouldHide = true;
hideReason = "Sponsored Content";
}
// 2. Keyword/Phrase Filter
if (!shouldHide && containsPhrases(postText, settings.hideKeyPhrases)) {
shouldHide = true;
hideReason = "Keyword Match";
}
// 3. Author Filter
if (!shouldHide && settings.hideAuthors.some(author => author && postText.includes(author))) {
shouldHide = true;
hideReason = "Blocked Author";
}
// 4. Classifieds Filter (Non-Free)
if (!shouldHide && settings.hideNonFreeClassifieds && (postText.includes('For Sale') || postText.includes('Classifieds'))) {
const hasFree = postText.toLowerCase().includes('free');
const hasPrice = /\$\d+/.test(postText);
if (hasPrice && !hasFree) {
shouldHide = true;
hideReason = "Paid Classified";
}
}
// Apply Hiding
if (shouldHide) {
console.log(`[Nextdoor Filter] Hiding post: ${hideReason}`);
post.classList.add('gm-hidden-post');
return;
}
// 5. Add Manual Hide Link
if (settings.addHideLink) {
const actionBar = post.querySelector('div[data-testid="post-footer"], .post-footer, [class*="footer"]');
if (actionBar && !post.querySelector('.gm-hide-btn')) {
const hideBtn = document.createElement('span');
hideBtn.className = 'gm-hide-btn';
hideBtn.innerText = 'Quick Hide';
hideBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
post.style.transition = 'opacity 0.3s';
post.style.opacity = '0';
setTimeout(() => post.classList.add('gm-hidden-post'), 300);
};
actionBar.appendChild(hideBtn);
}
}
};
// --- Observer Engine ---
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const posts = node.querySelectorAll('div[data-testid="post-container"], article, [class*="FeedPost"]');
if (node.matches('div[data-testid="post-container"], article, [class*="FeedPost"]')) {
processPost(node);
}
posts.forEach(processPost);
}
}
});
const startObserver = () => {
const feedContainer = document.querySelector('main, #newsfeed_wrapper, [role="main"]');
if (feedContainer) {
observer.observe(feedContainer, { childList: true, subtree: true });
feedContainer.querySelectorAll('div[data-testid="post-container"], article, [class*="FeedPost"]').forEach(processPost);
} else {
setTimeout(startObserver, 1000);
}
};
// Menu Commands
GM_registerMenuCommand("Open Filter Settings", showSettings);
GM_registerMenuCommand("Reset to Defaults", () => {
if(confirm("Are you sure you want to reset all filter settings?")) {
GM_setValue('nd_settings', DEFAULT_SETTINGS);
location.reload();
}
});
startObserver();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment