Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save overflowy/75b2df8a5349e51c5f83b5a2719d80d1 to your computer and use it in GitHub Desktop.
HN Article Search
// ==UserScript==
// @name HN Article Search
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @connect hn.algolia.com
// ==/UserScript==
(function () {
"use strict";
// Exit if running in an iframe
if (window.self !== window.top) {
return;
}
function canonicalize(href) {
try {
const u = new URL(href);
u.search = "";
u.hash = "";
u.hostname = u.hostname.replace(/^www\./, "");
if (u.pathname.length > 1 && u.pathname.endsWith("/")) {
u.pathname = u.pathname.slice(0, -1);
}
return u.toString();
} catch {
return href.split("?")[0].split("#")[0];
}
}
// If we're on an HN item page, grab the submitted URL from the post itself
function resolvePageUrl() {
const loc = window.location;
const isHNItem =
(loc.hostname === "news.ycombinator.com" ||
loc.hostname === "hn.algolia.com") &&
loc.pathname === "/item";
if (isHNItem) {
// The submitted link lives in .titleline > a on news.ycombinator.com
const link = document.querySelector(
".titleline > a, .title > a.storylink",
);
if (link && link.href && !link.href.startsWith("item?")) {
return link.href;
}
// Self-post (Ask HN, Show HN text post) — nothing to search for
return null;
}
return loc.href;
}
const pageUrl = resolvePageUrl();
if (!pageUrl) {
// On an HN self-post; nothing meaningful to search
return;
}
const canonical = canonicalize(pageUrl);
const bareUrl = canonical.replace(/^https?:\/\//, "");
const searchUrl = `https://hn.algolia.com/?q=${encodeURIComponent(bareUrl)}`;
const apiUrl = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(bareUrl)}&restrictSearchableAttributes=url`;
let menuId = null;
function openSearch() {
GM_openInTab(searchUrl, { active: true, setParent: true });
}
function registerMenu(label) {
if (menuId !== null && typeof GM_unregisterMenuCommand === "function") {
try {
GM_unregisterMenuCommand(menuId);
} catch (e) {
/* some managers don't support it reliably */
}
}
menuId = GM_registerMenuCommand(label, openSearch);
}
registerMenu("Searching…");
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
timeout: 8000,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
const n = data.nbHits ?? 0;
const label =
n === 0 ? "No results" : `${n} result${n === 1 ? "" : "s"}`;
registerMenu(label);
} catch (e) {
registerMenu("Search this page");
}
},
onerror: () => registerMenu("Search this page"),
ontimeout: () => registerMenu("Search this page"),
});
})();
@overflowy
Copy link
Copy Markdown
Author

Video of how it works: https://cleanshot.com/share/bKjjp0WV

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