Skip to content

Instantly share code, notes, and snippets.

@overflowy
Last active April 19, 2026 00:35
Show Gist options
  • Select an option

  • Save overflowy/bf5d9aedffcd46242a253a3ddf1271b4 to your computer and use it in GitHub Desktop.

Select an option

Save overflowy/bf5d9aedffcd46242a253a3ddf1271b4 to your computer and use it in GitHub Desktop.
Hacker News Plus
// ==UserScript==
// @name HN +
// @match https://*.ycombinator.com/*
// @grant none
// @version 2.5.0
// @author overflowy@riseup.net
// @description Adds favicons to HN links, navigation menu for less known sections, and injects the about section into HN's native user hovercard
// @inject-into content
// ==/UserScript==
// Add favicons functionality
var favicons = document.getElementsByClassName("favicon");
if (!(favicons.length > 0)) {
const articleLinks = document.querySelectorAll(".titleline > a");
for (let link of articleLinks) {
const domain = new URL(link.href).hostname;
const imageUrl = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
const imgEl = document.createElement("img");
imgEl.src = imageUrl;
imgEl.className = "favicon";
imgEl.width = 14;
imgEl.height = 14;
imgEl.style.paddingRight = "0.25em";
imgEl.style.paddingLeft = "0.25em";
link.style.alignItems = "center";
link.style.display = "inline-flex";
link.style.justifyContent = "center";
link.prepend(imgEl);
}
}
// Add navigation menu
function createNavigationMenu() {
// Look for the submit link in the main navigation
const submitLink = document.querySelector('.pagetop a[href="submit"]');
if (!submitLink) return;
// Create the menu button styled like other HN links
const menuButton = document.createElement("a");
menuButton.href = "#";
menuButton.textContent = "extra";
menuButton.style.cssText = "color: #000000; text-decoration: none;";
// Create the dropdown menu
const dropdown = document.createElement("div");
dropdown.className = "hn-dropdown";
dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
background: #f6f6ef;
border: 1px solid #ff6600;
z-index: 1000;
display: none;
min-width: 120px;
margin-top: 2px;
font-size: 10pt;
`;
const menuItems = [
{ url: "https://news.ycombinator.com/shownew", label: "shownew" },
{ url: "https://news.ycombinator.com/pool", label: "pool" },
{ url: "https://news.ycombinator.com/best", label: "best" },
{ url: "https://news.ycombinator.com/asknew", label: "asknew" },
{ url: "https://news.ycombinator.com/bestcomments", label: "bestcomments" },
{ url: "https://news.ycombinator.com/active", label: "active" },
{ url: "https://news.ycombinator.com/noobcomments", label: "newcomments" },
{ url: "https://news.ycombinator.com/noobstories", label: "newstories" },
{ url: "https://news.ycombinator.com/newest", label: "newest" },
];
menuItems.forEach((item, index) => {
const menuItem = document.createElement("a");
menuItem.href = item.url;
menuItem.textContent = item.label;
menuItem.style.cssText = `
display: block;
padding: 4px 8px;
color: #000000;
text-decoration: none;
font-size: 10pt;
${index < menuItems.length - 1 ? "border-bottom: 1px solid #ff6600;" : ""}
`;
menuItem.onmouseover = function () {
this.style.backgroundColor = "#ffffff";
};
menuItem.onmouseout = function () {
this.style.backgroundColor = "transparent";
};
dropdown.appendChild(menuItem);
});
// Create a wrapper for positioning
const wrapper = document.createElement("span");
wrapper.style.position = "relative";
wrapper.style.display = "inline";
wrapper.appendChild(menuButton);
wrapper.appendChild(dropdown);
// Find the separator after submit and insert our menu
let nextNode = submitLink.nextSibling;
while (
nextNode &&
nextNode.nodeType === 3 &&
nextNode.textContent.trim() === ""
) {
nextNode = nextNode.nextSibling;
}
// Insert separator and the menu button after submit
const separator = document.createTextNode(" | ");
submitLink.parentNode.insertBefore(separator, nextNode);
submitLink.parentNode.insertBefore(wrapper, nextNode);
// Toggle menu visibility
menuButton.addEventListener("click", function (e) {
e.preventDefault();
dropdown.style.display =
dropdown.style.display === "none" ? "block" : "none";
});
// Close menu when clicking outside
document.addEventListener("click", function (e) {
if (!wrapper.contains(e.target)) {
dropdown.style.display = "none";
}
});
}
createNavigationMenu();
// Augment HN's native hovercard with the "about" section
(function augmentHovercard() {
// Style the native hovercard to have the HN-orange border like our old custom card.
const style = document.createElement("style");
style.textContent = `
.hovercard {
border: 1px solid #ff6600 !important;
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.15);
padding: 8px 10px !important;
}
.hovercard .hn-plus-about {
padding-top: 6px;
border-top: 1px solid #e0e0d8;
max-height: 240px;
overflow: auto;
word-wrap: break-word;
overflow-wrap: break-word;
font-size: 9pt;
line-height: 1.4;
}
.hovercard .hn-plus-about a { word-break: break-all; }
.hovercard .hn-plus-loading { color: #828282; font-size: 9pt; margin-top: 4px; }
`;
document.documentElement.appendChild(style);
// Cache about-sections across hovercards
const aboutCache = new Map();
const inFlight = new Map();
function fetchAbout(username) {
if (aboutCache.has(username)) {
return Promise.resolve(aboutCache.get(username));
}
if (inFlight.has(username)) {
return inFlight.get(username);
}
const url = `https://news.ycombinator.com/user?id=${encodeURIComponent(username)}`;
const p = fetch(url, { credentials: "same-origin" })
.then((r) => (r.ok ? r.text() : null))
.then((html) => {
if (html === null) {
aboutCache.set(username, null);
inFlight.delete(username);
return null;
}
const about = extractAbout(html);
aboutCache.set(username, about);
inFlight.delete(username);
return about;
})
.catch(() => {
inFlight.delete(username);
aboutCache.set(username, null);
return null;
});
inFlight.set(username, p);
return p;
}
function extractAbout(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const rows = doc.querySelectorAll("#hnmain table tr");
for (const row of rows) {
const cells = row.querySelectorAll("td");
if (cells.length < 2) continue;
if (cells[0].textContent.trim().toLowerCase() === "about:") {
const cell = cells[1];
// Rewrite relative links to absolute and open in new tab
cell.querySelectorAll("a[href]").forEach((a) => {
const href = a.getAttribute("href");
if (href && !/^https?:\/\//i.test(href) && !href.startsWith("#")) {
a.setAttribute(
"href",
"https://news.ycombinator.com/" + href.replace(/^\//, ""),
);
}
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
});
const html = cell.innerHTML.trim();
return html.length > 0 ? html : null;
}
}
return null;
}
function getUsernameFromHovercard(hovercard) {
const link = hovercard.querySelector("a.hnuser");
if (!link) return null;
// The username is the text content of the <a class="hnuser">
return link.textContent.trim();
}
function injectAbout(hovercard) {
// Skip if already injected
if (hovercard.querySelector(".hn-plus-about, .hn-plus-loading")) return;
const username = getUsernameFromHovercard(hovercard);
if (!username) return;
// Find the <table> inside the hovercard and append a new row beneath the karma row
// so the about text appears between karma and the comments/mute/notes rows.
// Simpler: just append a <div> after the table.
const table = hovercard.querySelector("table");
if (!table) return;
// Insert loading placeholder right after the table
const loadingEl = document.createElement("div");
loadingEl.className = "hn-plus-loading";
loadingEl.textContent = "Loading about…";
table.insertAdjacentElement("afterend", loadingEl);
fetchAbout(username).then((about) => {
// Make sure this hovercard is still in the DOM
if (!hovercard.isConnected) return;
loadingEl.remove();
if (!about) return;
const aboutEl = document.createElement("div");
aboutEl.className = "hn-plus-about";
aboutEl.innerHTML = about;
table.insertAdjacentElement("afterend", aboutEl);
});
}
// Watch the document for hovercards appearing (HN toggles popover="auto" opacity/display).
// We observe attribute changes on any .hovercard element since HN reuses them.
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
// New nodes added (hovercard can be inserted on demand)
m.addedNodes.forEach((node) => {
if (node.nodeType !== 1) return;
if (node.matches && node.matches(".hovercard")) {
injectAbout(node);
} else if (node.querySelectorAll) {
node.querySelectorAll(".hovercard").forEach(injectAbout);
}
});
// Attribute changes on existing hovercards (e.g. opacity going from 0 to 1)
if (
m.type === "attributes" &&
m.target.classList &&
m.target.classList.contains("hovercard")
) {
injectAbout(m.target);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
// Catch any hovercards already in the DOM at script start
document.querySelectorAll(".hovercard").forEach(injectAbout);
})();
@sachahjkl
Copy link
Copy Markdown

here's an alternative that uses the google API to fetch the favicons. Don't know why but on my devices, all the favicons coming from the duckduckgo API render out to a grey circled arrow.

Thank you for this cool user-script though !

// ==UserScript==
// @name        Favicons for HN
// @namespace   Violentmonkey Scripts
// @match https://*.ycombinator.com/*
// @grant       none
// @version     1.0
// @author      sacha@sacha.house
// @description 07/06/2023, 08:48:00 AM
// @inject-into content
// ==/UserScript==

var favicons = document.getElementsByClassName("favicon");
if (!(favicons.length > 0)) {
	const articleLinks = document.querySelectorAll(".titleline > a");
	for (let link of articleLinks) {
		const domain = new URL(link.href).hostname;
                const imageUrl = `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=32`;
		const imgEl = document.createElement("img");
		imgEl.src = imageUrl;
		imgEl.className = "favicon";
		imgEl.width = 14;
		imgEl.height = 14;
		imgEl.style.paddingRight = "0.25em";
		imgEl.style.paddingLeft = "0.25em";
		link.style.alignItems = "center";
		link.style.display = "inline-flex";
		link.style.justifyContent = "center";
		link.prepend(imgEl);
	}
}

@polyrabbit
Copy link
Copy Markdown

Alternative - https://hackernews.betacat.io/

with:

  • favicon
  • summary
  • illustration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment