HN Article Search

2 min read Original article ↗
// ==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"), }); })();