From e2b292eb50fa4e9b7f38e5f1a9368300f4650147 Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 06:55:49 +0000 Subject: [PATCH 1/8] fix(results): support current Google result selectors - Parse modern Google result and rich-result layouts - Reorder only standalone results to preserve grouped modules --- .../src/entrypoints/content/index.tsx | 2 +- apps/extension/src/utils/filter.ts | 117 ++++++++---------- 2 files changed, 51 insertions(+), 68 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index 359a466..bf35a1b 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -77,7 +77,7 @@ function reorderResults( function sortResults(results: Results, rankings: RankingsV2 | null) { const rankedResults = orderedResults(results, rankings); - reorderResults(rankedResults); + reorderResults(rankedResults.filter((result) => result.canReorder)); } function addPopupContainers(searches: Results) { diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 19f7009..37de984 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -1,36 +1,35 @@ import $ from "jquery"; import { err, ok } from "neverthrow"; -// for refrence see https://github.com/benbusby/whoogle-search/blob/e4cabe3e5b9aa55cc14f845bb3e194d83d46ed1c/app/filter.py -// https://github.com/searxng/searxng/blob/885d02c8c3a3ae54177eab81e672abe65a76acf5/searx/engines/google.py - -const ITEM_PINNED_RESULT_CLASS = "BYM4Nd"; -const JSCONTROLLER_RESULT = "SC7lYd"; - -const JSNAME_LINK_ID = "UWckNb"; - -// const BLOCK_TITLES: string[] = [ -// "People also ask", -// "Related searches", -// "Videos", -// "Top stories", -// "Images", -// "News", -// ]; - -// Extend jQuery with custom methods -// declare global { -// interface JQuery { -// log(): JQuery; -// } -// } - -// // Add the log method to jQuery -// $.fn.log = function (this: JQuery) { -// // Convert jQuery object to array and spread it for console.log -// console.log.apply(console, this as any); -// return this; -// }; +// Keep these in sync with uBlacklist's desktop Google web selectors. +// https://github.com/ublacklist/builtin/blob/master/serpinfo/google.yml +const RESULT_SELECTORS = [ + { + root: ".vt6azd:not(.g-blk), .Ww4FFb", + url: ":is(.yuRUbf, .xe8e1b) a", + title: "h3", + }, + { root: ".vCUuC", url: "a", title: ".Yt787" }, + { root: ".sHEJob", url: 'a[href^="http"]', title: ".OSrXXb" }, + { + root: "[data-news-cluster-id]", + url: "a", + title: '[role="heading"][aria-level="3"]', + }, + { root: ".eejeod", url: "a", title: "h3" }, + { + root: ".ivg-i:not(.my5z3d)", + url: ".EZAeBe", + title: ".OSrXXb", + }, + { root: ".ivg-i.my5z3d", url: ".LBcIee", title: ".ddBkwd" }, +] as const; + +const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( + ", ", +); + +const REORDERABLE_RESULT_SELECTOR = '[jscontroller="SC7lYd"], .BYM4Nd'; export function getResults() { const searches = extractDomains() @@ -54,11 +53,8 @@ function extractDomains() { return []; } - // Filter out unwanted sections const blocks = $rso - .find( - `[jscontroller="${JSCONTROLLER_RESULT}"], .${ITEM_PINNED_RESULT_CLASS}`, - ) + .find(RESULT_ROOT_SELECTOR) .map((_, element) => $(element)) .toArray(); @@ -68,42 +64,29 @@ function extractDomains() { } function parseBlock(element: JQuery) { - const href = element - .find("a") - .filter(`[jsname="${JSNAME_LINK_ID}"]`) - .map((_, element) => ({ - href: $(element).attr("href"), - text: $(element).find("h3").text(), - })) - .get(); - - const isResult = element.is(`[jscontroller="${JSCONTROLLER_RESULT}"]`); - const isPinnedResult = element.is(`.${ITEM_PINNED_RESULT_CLASS}`); - - if (isResult || isPinnedResult) { - if (href[0]?.href === undefined) { - return err({ - error: "could_not_parse_domain" as const, - element, - }); - } - return ok({ - domain: getHostnames(href[0].href), - text: href[0].text, - elementType: "result" as const, - element, - }); - } else if (href.length === 0) { - return ok({ - elementType: "empty" as const, - element, - }); - } else { - return ok({ - elementType: "special" as const, + const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); + const href = selectors + ? element.find(selectors.url).first().attr("href") + : undefined; + + if (!selectors || href === undefined) { + return err({ + error: "could_not_parse_domain" as const, element, }); } + + return ok({ + domain: getHostnames(href), + text: element.find(selectors.title).first().text(), + elementType: "result" as const, + // Rich result cards are safe to block individually, but moving them would + // pull them out of their containing news, image, or video module. + canReorder: + element.is(REORDERABLE_RESULT_SELECTOR) && + element.parents(REORDERABLE_RESULT_SELECTOR).length === 0, + element, + }); } function getHostnames(url: string) { From 49a84ea35caade49a423f94a2832f9d1ddedaee5 Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:36:07 +0000 Subject: [PATCH 2/8] fix(results): safely parse result domains - Return a parse error for unsupported result selectors - Handle missing or invalid result URLs without throwing --- apps/extension/src/utils/filter.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 37de984..b66b843 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -65,11 +65,17 @@ function extractDomains() { function parseBlock(element: JQuery) { const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); - const href = selectors - ? element.find(selectors.url).first().attr("href") - : undefined; + if (!selectors) { + return err({ + error: "could_not_parse_domain" as const, + element, + }); + } + + const href = element.find(selectors.url).first().attr("href"); + const domain = href ? getHostname(href) : null; - if (!selectors || href === undefined) { + if (!domain) { return err({ error: "could_not_parse_domain" as const, element, @@ -77,7 +83,7 @@ function parseBlock(element: JQuery) { } return ok({ - domain: getHostnames(href), + domain, text: element.find(selectors.title).first().text(), elementType: "result" as const, // Rich result cards are safe to block individually, but moving them would @@ -89,13 +95,10 @@ function parseBlock(element: JQuery) { }); } -function getHostnames(url: string) { +function getHostname(url: string) { try { - if (url.startsWith("/") || url.startsWith("#")) { - throw new Error("Invalid URL"); - } - return new URL(url).hostname; + return new URL(url).hostname || null; } catch { - throw new Error("Invalid URL"); + return null; } } From 8417e45db8190fd741f48965c6696c69425c9180 Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:42:56 +0000 Subject: [PATCH 3/8] fix(results): remove blocked cards before reordering - Classify primary results separately from rich-result cards - Avoid reordering nested result modules --- .../src/entrypoints/content/index.tsx | 12 ++++++--- apps/extension/src/utils/filter.ts | 27 +++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index bf35a1b..9508dbd 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -33,7 +33,6 @@ function orderedResults(results: Results, rankings: RankingsV2 | null) { case "lower": return { ...result, ord: order - weight, rank }; case "block": - result.element.remove(); return null; // return { ...result, ord: order - 9999, rank }; default: @@ -76,8 +75,15 @@ function reorderResults( } function sortResults(results: Results, rankings: RankingsV2 | null) { - const rankedResults = orderedResults(results, rankings); - reorderResults(rankedResults.filter((result) => result.canReorder)); + results + .filter((result) => rankings?.[result.domain]?.type === "block") + .forEach((result) => { + result.element.remove(); + }); + + const reorderableResults = results.filter((result) => result.canReorder); + const rankedResults = orderedResults(reorderableResults, rankings); + reorderResults(rankedResults); } function addPopupContainers(searches: Results) { diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index b66b843..3cc1e53 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -5,32 +5,43 @@ import { err, ok } from "neverthrow"; // https://github.com/ublacklist/builtin/blob/master/serpinfo/google.yml const RESULT_SELECTORS = [ { + type: "result", root: ".vt6azd:not(.g-blk), .Ww4FFb", url: ":is(.yuRUbf, .xe8e1b) a", title: "h3", }, - { root: ".vCUuC", url: "a", title: ".Yt787" }, - { root: ".sHEJob", url: 'a[href^="http"]', title: ".OSrXXb" }, + { type: "card", root: ".vCUuC", url: "a", title: ".Yt787" }, { + type: "card", + root: ".sHEJob", + url: 'a[href^="http"]', + title: ".OSrXXb", + }, + { + type: "card", root: "[data-news-cluster-id]", url: "a", title: '[role="heading"][aria-level="3"]', }, - { root: ".eejeod", url: "a", title: "h3" }, + { type: "card", root: ".eejeod", url: "a", title: "h3" }, { + type: "card", root: ".ivg-i:not(.my5z3d)", url: ".EZAeBe", title: ".OSrXXb", }, - { root: ".ivg-i.my5z3d", url: ".LBcIee", title: ".ddBkwd" }, + { + type: "card", + root: ".ivg-i.my5z3d", + url: ".LBcIee", + title: ".ddBkwd", + }, ] as const; const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( ", ", ); -const REORDERABLE_RESULT_SELECTOR = '[jscontroller="SC7lYd"], .BYM4Nd'; - export function getResults() { const searches = extractDomains() .map((s) => { @@ -89,8 +100,8 @@ function parseBlock(element: JQuery) { // Rich result cards are safe to block individually, but moving them would // pull them out of their containing news, image, or video module. canReorder: - element.is(REORDERABLE_RESULT_SELECTOR) && - element.parents(REORDERABLE_RESULT_SELECTOR).length === 0, + selectors.type === "result" && + element.parents(selectors.root).length === 0, element, }); } From 5505d0deaf14a802e25b94daa3e984cf8a88d24f Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:56:50 +0000 Subject: [PATCH 4/8] add debug logging to result parsing pipeline - Log result extraction, sorting, decoration, and content-script lifecycle - Report selector matches and parsing failures for troubleshooting --- .../src/entrypoints/content/index.tsx | 54 ++++++++++++++++++- apps/extension/src/utils/filter.ts | 50 ++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index 9508dbd..fd1239f 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -7,6 +7,8 @@ import { getResults, type Results } from "@/utils/filter"; import { items, type RankingsV2 } from "@/utils/storage"; import { getPageTheme } from "@/utils/theme"; +const LOG_PREFIX = "[SearchTuner]"; + const RERANK_WEIGHTS = { weak: 1, normal: 3, @@ -75,6 +77,14 @@ function reorderResults( } function sortResults(results: Results, rankings: RankingsV2 | null) { + console.log(`${LOG_PREFIX} sorting`, { + total: results.length, + reorderable: results.filter((result) => result.canReorder).length, + blocked: results.filter( + (result) => rankings?.[result.domain]?.type === "block", + ).length, + }); + results .filter((result) => rankings?.[result.domain]?.type === "block") .forEach((result) => { @@ -87,12 +97,21 @@ function sortResults(results: Results, rankings: RankingsV2 | null) { } function addPopupContainers(searches: Results) { + console.log(`${LOG_PREFIX} decorating results`, { + count: searches.length, + results: searches.map((search) => ({ + domain: search.domain, + text: search.text, + element: search.element[0], + })), + }); + const theme = getPageTheme(); const template = document.createElement("div"); template.classList.add("searchtuner-container"); template.setAttribute("data-theme", theme); - searches.forEach((search) => { + searches.forEach((search, index) => { // Ensure the parent is positioned relatively so absolute works const parent = search.element[0]; if (getComputedStyle(parent).position === "static") { @@ -102,6 +121,11 @@ function addPopupContainers(searches: Results) { const container = template.cloneNode(true); parent.appendChild(container); render(() => , container); + console.log(`${LOG_PREFIX} decorated result ${index + 1}`, { + domain: search.domain, + result: parent, + container, + }); }); } @@ -129,19 +153,34 @@ function main(config: { rankings_active: boolean; rankings: RankingsV2 | null; }) { - if (!config.rankings_active) return; + console.log(`${LOG_PREFIX} main`, { + url: location.href, + rankingsActive: config.rankings_active, + rankingCount: config.rankings ? Object.keys(config.rankings).length : 0, + }); + if (!config.rankings_active) { + console.warn(`${LOG_PREFIX} rankings are disabled; skipping page`); + return; + } const searches = getResults(); sortResults(searches, config.rankings); addPopupContainers(searches); + console.log(`${LOG_PREFIX} main complete`, { + results: searches.length, + decorations: document.querySelectorAll(".searchtuner-container").length, + }); } function runOnBody(condition: () => boolean, callback: () => void) { if (condition()) { + console.log(`${LOG_PREFIX} #rso was already present`); callback(); } else { + console.log(`${LOG_PREFIX} waiting for #rso`); const observer = new MutationObserver((_mutations, obs) => { performance.mark("ST_mutationObserver"); if (condition()) { + console.log(`${LOG_PREFIX} found #rso after DOM mutation`); obs.disconnect(); // Stop observing once element is found callback(); } @@ -157,20 +196,31 @@ export default defineContentScript({ matches: getGoogleDomains(), runAt: "document_start", main() { + console.log(`${LOG_PREFIX} content script started`, { + url: location.href, + readyState: document.readyState, + }); hideMain(); const configPromise = getConfig(); + configPromise.then((config) => { + console.log(`${LOG_PREFIX} loaded config`, config); + }); // backup to show main if the config is not active configPromise.then((config) => { if (!config.rankings_active) showMain(); }); const timeout = setTimeout(() => showMain(), 1000); document.addEventListener("DOMContentLoaded", () => { + console.log(`${LOG_PREFIX} DOMContentLoaded`); runOnBody( () => !!$("div#rso").length, () => { clearTimeout(timeout); configPromise .then((config) => main(config)) + .catch((error) => { + console.error(`${LOG_PREFIX} main failed`, error); + }) .finally(() => showMain()); }, ); diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 3cc1e53..1b2e045 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -1,6 +1,8 @@ import $ from "jquery"; import { err, ok } from "neverthrow"; +const LOG_PREFIX = "[SearchTuner]"; + // Keep these in sync with uBlacklist's desktop Google web selectors. // https://github.com/ublacklist/builtin/blob/master/serpinfo/google.yml const RESULT_SELECTORS = [ @@ -43,7 +45,8 @@ const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( ); export function getResults() { - const searches = extractDomains() + const extracted = extractDomains(); + const searches = extracted .map((s) => { if (s.isErr() || s.value.elementType !== "result") { return null; @@ -51,6 +54,20 @@ export function getResults() { return s.value; }) .filter((s) => s !== null); + + console.log(`${LOG_PREFIX} parsed results`, { + extracted: extracted.length, + returned: searches.length, + reorderable: searches.filter((result) => result.canReorder).length, + cards: searches.filter((result) => !result.canReorder).length, + results: searches.map((result) => ({ + domain: result.domain, + text: result.text, + canReorder: result.canReorder, + element: result.element[0], + })), + }); + return searches; } @@ -60,14 +77,32 @@ function extractDomains() { // Get the main results container const $rso = $("div#rso"); if ($rso.length === 0) { - console.error("Could not find result container #rso"); + console.error(`${LOG_PREFIX} could not find result container #rso`); return []; } + const selectorCounts = RESULT_SELECTORS.map((selectors) => ({ + type: selectors.type, + root: selectors.root, + url: selectors.url, + matches: $rso.find(selectors.root).length, + })); + console.log(`${LOG_PREFIX} selector scan`, { + url: location.href, + rso: $rso[0], + combinedSelector: RESULT_ROOT_SELECTOR, + selectorCounts, + }); + console.table(selectorCounts); + const blocks = $rso .find(RESULT_ROOT_SELECTOR) .map((_, element) => $(element)) .toArray(); + console.log(`${LOG_PREFIX} matched DOM blocks`, { + count: blocks.length, + elements: blocks.map((block) => block[0]), + }); const results = blocks.map(parseBlock); @@ -77,6 +112,9 @@ function extractDomains() { function parseBlock(element: JQuery) { const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); if (!selectors) { + console.warn(`${LOG_PREFIX} rejected block: no matching definition`, { + element: element[0], + }); return err({ error: "could_not_parse_domain" as const, element, @@ -87,6 +125,14 @@ function parseBlock(element: JQuery) { const domain = href ? getHostname(href) : null; if (!domain) { + console.warn(`${LOG_PREFIX} rejected block: invalid or missing URL`, { + type: selectors.type, + rootSelector: selectors.root, + urlSelector: selectors.url, + href, + link: element.find(selectors.url).first()[0], + element: element[0], + }); return err({ error: "could_not_parse_domain" as const, element, From e05e0508b4e99458a82ecda8dd52768a6ba0247c Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:39:54 +0000 Subject: [PATCH 5/8] fix(results): resolve domains from citations and redirects - Await asynchronous result parsing - Track the source used to resolve each domain --- .../src/entrypoints/content/index.tsx | 4 +- apps/extension/src/utils/filter.ts | 90 ++++++++++++++++--- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index fd1239f..aa0355d 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -149,7 +149,7 @@ const getConfig = async () => { return { rankings_active, rankings }; }; -function main(config: { +async function main(config: { rankings_active: boolean; rankings: RankingsV2 | null; }) { @@ -162,7 +162,7 @@ function main(config: { console.warn(`${LOG_PREFIX} rankings are disabled; skipping page`); return; } - const searches = getResults(); + const searches = await getResults(); sortResults(searches, config.rankings); addPopupContainers(searches); console.log(`${LOG_PREFIX} main complete`, { diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 1b2e045..e403565 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -44,14 +44,11 @@ const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( ", ", ); -export function getResults() { - const extracted = extractDomains(); +export async function getResults() { + const extracted = await extractDomains(); const searches = extracted .map((s) => { - if (s.isErr() || s.value.elementType !== "result") { - return null; - } - return s.value; + return s.isOk() ? s.value : null; }) .filter((s) => s !== null); @@ -62,6 +59,7 @@ export function getResults() { cards: searches.filter((result) => !result.canReorder).length, results: searches.map((result) => ({ domain: result.domain, + domainSource: result.domainSource, text: result.text, canReorder: result.canReorder, element: result.element[0], @@ -71,9 +69,9 @@ export function getResults() { return searches; } -export type Results = ReturnType; +export type Results = Awaited>; -function extractDomains() { +async function extractDomains() { // Get the main results container const $rso = $("div#rso"); if ($rso.length === 0) { @@ -104,12 +102,12 @@ function extractDomains() { elements: blocks.map((block) => block[0]), }); - const results = blocks.map(parseBlock); + const results = await Promise.all(blocks.map(parseBlock)); return results; } -function parseBlock(element: JQuery) { +async function parseBlock(element: JQuery) { const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); if (!selectors) { console.warn(`${LOG_PREFIX} rejected block: no matching definition`, { @@ -122,7 +120,15 @@ function parseBlock(element: JQuery) { } const href = element.find(selectors.url).first().attr("href"); - const domain = href ? getHostname(href) : null; + const hrefDomain = href ? getHostname(href) : null; + const cite = element.find("cite").first().text(); + const citeDomain = + selectors.type === "result" ? getCitedHostname(cite) : null; + const redirectDomain = + href && !hrefDomain && !citeDomain + ? await getGoogleRedirectHostname(href) + : null; + const domain = hrefDomain ?? citeDomain ?? redirectDomain; if (!domain) { console.warn(`${LOG_PREFIX} rejected block: invalid or missing URL`, { @@ -130,6 +136,7 @@ function parseBlock(element: JQuery) { rootSelector: selectors.root, urlSelector: selectors.url, href, + cite, link: element.find(selectors.url).first()[0], element: element[0], }); @@ -141,8 +148,12 @@ function parseBlock(element: JQuery) { return ok({ domain, + domainSource: hrefDomain + ? ("href" as const) + : citeDomain + ? ("cite" as const) + : ("redirect" as const), text: element.find(selectors.title).first().text(), - elementType: "result" as const, // Rich result cards are safe to block individually, but moving them would // pull them out of their containing news, image, or video module. canReorder: @@ -159,3 +170,58 @@ function getHostname(url: string) { return null; } } + +function getCitedHostname(cite: string) { + const [origin] = cite.trim().split(/\s+[›·]\s+/); + if (!origin) return null; + + const hostname = getHostname(origin) ?? getHostname(`https://${origin}`); + return hostname?.includes(".") ? hostname : null; +} + +async function getGoogleRedirectHostname(href: string) { + let redirectUrl: URL; + try { + redirectUrl = new URL(href, location.href); + } catch { + return null; + } + + if ( + redirectUrl.origin !== location.origin || + !["/goto", "/url"].includes(redirectUrl.pathname) + ) { + return null; + } + + const parameterDomain = ["url", "q"] + .map((parameter) => redirectUrl.searchParams.get(parameter)) + .map((url) => (url ? getHostname(url) : null)) + .find((hostname) => hostname !== null); + if (parameterDomain) return parameterDomain; + + try { + // Current Google variants encrypt outbound hrefs. The same-origin redirect + // page still exposes the destination as a normal link. + const response = await fetch(redirectUrl, { credentials: "include" }); + const responseDomain = getHostname(response.url); + if (responseDomain && responseDomain !== location.hostname) { + return responseDomain; + } + + const redirectDocument = new DOMParser().parseFromString( + await response.text(), + "text/html", + ); + const destination = redirectDocument + .querySelector('a[href^="http"]') + ?.getAttribute("href"); + return destination ? getHostname(destination) : null; + } catch (error) { + console.warn(`${LOG_PREFIX} could not resolve Google redirect`, { + href, + error, + }); + return null; + } +} From 108b6b68d67cb460d4c25e5367436fa8d01ad53f Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:39:20 +0000 Subject: [PATCH 6/8] fix(results): parse result domains synchronously - Remove async Google redirect resolution - Skip results without visible domains --- .../src/entrypoints/content/index.tsx | 4 +- apps/extension/src/utils/filter.ts | 73 +++---------------- 2 files changed, 11 insertions(+), 66 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index aa0355d..fd1239f 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -149,7 +149,7 @@ const getConfig = async () => { return { rankings_active, rankings }; }; -async function main(config: { +function main(config: { rankings_active: boolean; rankings: RankingsV2 | null; }) { @@ -162,7 +162,7 @@ async function main(config: { console.warn(`${LOG_PREFIX} rankings are disabled; skipping page`); return; } - const searches = await getResults(); + const searches = getResults(); sortResults(searches, config.rankings); addPopupContainers(searches); console.log(`${LOG_PREFIX} main complete`, { diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index e403565..92d782c 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -44,8 +44,8 @@ const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( ", ", ); -export async function getResults() { - const extracted = await extractDomains(); +export function getResults() { + const extracted = extractDomains(); const searches = extracted .map((s) => { return s.isOk() ? s.value : null; @@ -69,9 +69,9 @@ export async function getResults() { return searches; } -export type Results = Awaited>; +export type Results = ReturnType; -async function extractDomains() { +function extractDomains() { // Get the main results container const $rso = $("div#rso"); if ($rso.length === 0) { @@ -102,12 +102,12 @@ async function extractDomains() { elements: blocks.map((block) => block[0]), }); - const results = await Promise.all(blocks.map(parseBlock)); + const results = blocks.map(parseBlock); return results; } -async function parseBlock(element: JQuery) { +function parseBlock(element: JQuery) { const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); if (!selectors) { console.warn(`${LOG_PREFIX} rejected block: no matching definition`, { @@ -124,14 +124,10 @@ async function parseBlock(element: JQuery) { const cite = element.find("cite").first().text(); const citeDomain = selectors.type === "result" ? getCitedHostname(cite) : null; - const redirectDomain = - href && !hrefDomain && !citeDomain - ? await getGoogleRedirectHostname(href) - : null; - const domain = hrefDomain ?? citeDomain ?? redirectDomain; + const domain = hrefDomain ?? citeDomain; if (!domain) { - console.warn(`${LOG_PREFIX} rejected block: invalid or missing URL`, { + console.debug(`${LOG_PREFIX} skipped block: no visible domain`, { type: selectors.type, rootSelector: selectors.root, urlSelector: selectors.url, @@ -148,11 +144,7 @@ async function parseBlock(element: JQuery) { return ok({ domain, - domainSource: hrefDomain - ? ("href" as const) - : citeDomain - ? ("cite" as const) - : ("redirect" as const), + domainSource: hrefDomain ? ("href" as const) : ("cite" as const), text: element.find(selectors.title).first().text(), // Rich result cards are safe to block individually, but moving them would // pull them out of their containing news, image, or video module. @@ -178,50 +170,3 @@ function getCitedHostname(cite: string) { const hostname = getHostname(origin) ?? getHostname(`https://${origin}`); return hostname?.includes(".") ? hostname : null; } - -async function getGoogleRedirectHostname(href: string) { - let redirectUrl: URL; - try { - redirectUrl = new URL(href, location.href); - } catch { - return null; - } - - if ( - redirectUrl.origin !== location.origin || - !["/goto", "/url"].includes(redirectUrl.pathname) - ) { - return null; - } - - const parameterDomain = ["url", "q"] - .map((parameter) => redirectUrl.searchParams.get(parameter)) - .map((url) => (url ? getHostname(url) : null)) - .find((hostname) => hostname !== null); - if (parameterDomain) return parameterDomain; - - try { - // Current Google variants encrypt outbound hrefs. The same-origin redirect - // page still exposes the destination as a normal link. - const response = await fetch(redirectUrl, { credentials: "include" }); - const responseDomain = getHostname(response.url); - if (responseDomain && responseDomain !== location.hostname) { - return responseDomain; - } - - const redirectDocument = new DOMParser().parseFromString( - await response.text(), - "text/html", - ); - const destination = redirectDocument - .querySelector('a[href^="http"]') - ?.getAttribute("href"); - return destination ? getHostname(destination) : null; - } catch (error) { - console.warn(`${LOG_PREFIX} could not resolve Google redirect`, { - href, - error, - }); - return null; - } -} From aaf0480ef5bde761cfd517d8af559e27d8421953 Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:41:27 +0000 Subject: [PATCH 7/8] fix(results): scope links to their result block - Prevent nested result links from being parsed incorrectly - Remove verbose result parsing and content script debug logs --- .../src/entrypoints/content/index.tsx | 72 ++++--------------- apps/extension/src/utils/filter.ts | 60 ++-------------- 2 files changed, 22 insertions(+), 110 deletions(-) diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index fd1239f..8c15fc4 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -7,8 +7,6 @@ import { getResults, type Results } from "@/utils/filter"; import { items, type RankingsV2 } from "@/utils/storage"; import { getPageTheme } from "@/utils/theme"; -const LOG_PREFIX = "[SearchTuner]"; - const RERANK_WEIGHTS = { weak: 1, normal: 3, @@ -77,41 +75,29 @@ function reorderResults( } function sortResults(results: Results, rankings: RankingsV2 | null) { - console.log(`${LOG_PREFIX} sorting`, { - total: results.length, - reorderable: results.filter((result) => result.canReorder).length, - blocked: results.filter( - (result) => rankings?.[result.domain]?.type === "block", - ).length, - }); - - results - .filter((result) => rankings?.[result.domain]?.type === "block") - .forEach((result) => { + const visibleResults = results.filter((result) => { + if (rankings?.[result.domain]?.type === "block") { result.element.remove(); - }); + return false; + } + return true; + }); - const reorderableResults = results.filter((result) => result.canReorder); + const reorderableResults = visibleResults.filter( + (result) => result.canReorder, + ); const rankedResults = orderedResults(reorderableResults, rankings); reorderResults(rankedResults); + return visibleResults; } function addPopupContainers(searches: Results) { - console.log(`${LOG_PREFIX} decorating results`, { - count: searches.length, - results: searches.map((search) => ({ - domain: search.domain, - text: search.text, - element: search.element[0], - })), - }); - const theme = getPageTheme(); const template = document.createElement("div"); template.classList.add("searchtuner-container"); template.setAttribute("data-theme", theme); - searches.forEach((search, index) => { + searches.forEach((search) => { // Ensure the parent is positioned relatively so absolute works const parent = search.element[0]; if (getComputedStyle(parent).position === "static") { @@ -121,11 +107,6 @@ function addPopupContainers(searches: Results) { const container = template.cloneNode(true); parent.appendChild(container); render(() => , container); - console.log(`${LOG_PREFIX} decorated result ${index + 1}`, { - domain: search.domain, - result: parent, - container, - }); }); } @@ -153,34 +134,19 @@ function main(config: { rankings_active: boolean; rankings: RankingsV2 | null; }) { - console.log(`${LOG_PREFIX} main`, { - url: location.href, - rankingsActive: config.rankings_active, - rankingCount: config.rankings ? Object.keys(config.rankings).length : 0, - }); - if (!config.rankings_active) { - console.warn(`${LOG_PREFIX} rankings are disabled; skipping page`); - return; - } + if (!config.rankings_active) return; const searches = getResults(); - sortResults(searches, config.rankings); - addPopupContainers(searches); - console.log(`${LOG_PREFIX} main complete`, { - results: searches.length, - decorations: document.querySelectorAll(".searchtuner-container").length, - }); + const visibleSearches = sortResults(searches, config.rankings); + addPopupContainers(visibleSearches); } function runOnBody(condition: () => boolean, callback: () => void) { if (condition()) { - console.log(`${LOG_PREFIX} #rso was already present`); callback(); } else { - console.log(`${LOG_PREFIX} waiting for #rso`); const observer = new MutationObserver((_mutations, obs) => { performance.mark("ST_mutationObserver"); if (condition()) { - console.log(`${LOG_PREFIX} found #rso after DOM mutation`); obs.disconnect(); // Stop observing once element is found callback(); } @@ -196,22 +162,14 @@ export default defineContentScript({ matches: getGoogleDomains(), runAt: "document_start", main() { - console.log(`${LOG_PREFIX} content script started`, { - url: location.href, - readyState: document.readyState, - }); hideMain(); const configPromise = getConfig(); - configPromise.then((config) => { - console.log(`${LOG_PREFIX} loaded config`, config); - }); // backup to show main if the config is not active configPromise.then((config) => { if (!config.rankings_active) showMain(); }); const timeout = setTimeout(() => showMain(), 1000); document.addEventListener("DOMContentLoaded", () => { - console.log(`${LOG_PREFIX} DOMContentLoaded`); runOnBody( () => !!$("div#rso").length, () => { @@ -219,7 +177,7 @@ export default defineContentScript({ configPromise .then((config) => main(config)) .catch((error) => { - console.error(`${LOG_PREFIX} main failed`, error); + console.error("[SearchTuner] main failed", error); }) .finally(() => showMain()); }, diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 92d782c..242b137 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -1,8 +1,6 @@ import $ from "jquery"; import { err, ok } from "neverthrow"; -const LOG_PREFIX = "[SearchTuner]"; - // Keep these in sync with uBlacklist's desktop Google web selectors. // https://github.com/ublacklist/builtin/blob/master/serpinfo/google.yml const RESULT_SELECTORS = [ @@ -45,28 +43,11 @@ const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( ); export function getResults() { - const extracted = extractDomains(); - const searches = extracted + return extractDomains() .map((s) => { return s.isOk() ? s.value : null; }) .filter((s) => s !== null); - - console.log(`${LOG_PREFIX} parsed results`, { - extracted: extracted.length, - returned: searches.length, - reorderable: searches.filter((result) => result.canReorder).length, - cards: searches.filter((result) => !result.canReorder).length, - results: searches.map((result) => ({ - domain: result.domain, - domainSource: result.domainSource, - text: result.text, - canReorder: result.canReorder, - element: result.element[0], - })), - }); - - return searches; } export type Results = ReturnType; @@ -75,32 +56,13 @@ function extractDomains() { // Get the main results container const $rso = $("div#rso"); if ($rso.length === 0) { - console.error(`${LOG_PREFIX} could not find result container #rso`); return []; } - const selectorCounts = RESULT_SELECTORS.map((selectors) => ({ - type: selectors.type, - root: selectors.root, - url: selectors.url, - matches: $rso.find(selectors.root).length, - })); - console.log(`${LOG_PREFIX} selector scan`, { - url: location.href, - rso: $rso[0], - combinedSelector: RESULT_ROOT_SELECTOR, - selectorCounts, - }); - console.table(selectorCounts); - const blocks = $rso .find(RESULT_ROOT_SELECTOR) .map((_, element) => $(element)) .toArray(); - console.log(`${LOG_PREFIX} matched DOM blocks`, { - count: blocks.length, - elements: blocks.map((block) => block[0]), - }); const results = blocks.map(parseBlock); @@ -110,16 +72,17 @@ function extractDomains() { function parseBlock(element: JQuery) { const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); if (!selectors) { - console.warn(`${LOG_PREFIX} rejected block: no matching definition`, { - element: element[0], - }); return err({ error: "could_not_parse_domain" as const, element, }); } - const href = element.find(selectors.url).first().attr("href"); + const link = element + .find(selectors.url) + .filter((_, link) => $(link).closest(RESULT_ROOT_SELECTOR).is(element)) + .first(); + const href = link.attr("href"); const hrefDomain = href ? getHostname(href) : null; const cite = element.find("cite").first().text(); const citeDomain = @@ -127,15 +90,6 @@ function parseBlock(element: JQuery) { const domain = hrefDomain ?? citeDomain; if (!domain) { - console.debug(`${LOG_PREFIX} skipped block: no visible domain`, { - type: selectors.type, - rootSelector: selectors.root, - urlSelector: selectors.url, - href, - cite, - link: element.find(selectors.url).first()[0], - element: element[0], - }); return err({ error: "could_not_parse_domain" as const, element, @@ -150,7 +104,7 @@ function parseBlock(element: JQuery) { // pull them out of their containing news, image, or video module. canReorder: selectors.type === "result" && - element.parents(selectors.root).length === 0, + element.parents(RESULT_ROOT_SELECTOR).length === 0, element, }); } From 2e9e9daf4b6d45ee5b3d81cd2aa90f72fad8ebe1 Mon Sep 17 00:00:00 2001 From: Shubham Bhardwaj <15147944+ShoeBoom@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:46:53 +0000 Subject: [PATCH 8/8] fix(results): scope links and citations to result block --- apps/extension/src/utils/filter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 242b137..f920cb2 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -80,11 +80,11 @@ function parseBlock(element: JQuery) { const link = element .find(selectors.url) - .filter((_, link) => $(link).closest(RESULT_ROOT_SELECTOR).is(element)) + .filter((_, link) => link.closest(RESULT_ROOT_SELECTOR) === element[0]) .first(); const href = link.attr("href"); const hrefDomain = href ? getHostname(href) : null; - const cite = element.find("cite").first().text(); + const cite = link.find("cite").first().text(); const citeDomain = selectors.type === "result" ? getCitedHostname(cite) : null; const domain = hrefDomain ?? citeDomain;