Last active
April 17, 2026 19:22
-
-
Save overflowy/75b2df8a5349e51c5f83b5a2719d80d1 to your computer and use it in GitHub Desktop.
HN Article Search
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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"), | |
| }); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Video of how it works: https://cleanshot.com/share/bKjjp0WV