Skip to content

Search pages ​

What Anubis draws and changes on search pages. Part of Experiments and decisions. Newest notes go at the top of each section.

A check for bugs (2026-09-30) ​

The owner asked for a hard check for bugs and a polish. Read through the content script, the matching and list code, the background script, the popup, and Settings; each fix below has a check that failed before it.

  • Google tidying its address counted as a new search. Anubis took any change of address for a new search and started its page count again, so after Google rewrote its address the summary said one page, and an automatic load would fetch page 2 again, find only repeats, and now say so. A new search is now a change of path, query, tab, filter, or page (sameSearch in utils/engines.ts); other parameters don't count. The deeper part rewrites the address after loading a page: 1 page before, 2 after.
  • The ⚖ button didn't close its own menu. A press outside the menu closes it, and the button is inside a closed shadow root, so the page saw a press on its host, not on the button: the menu closed on the press and opened again on the click. The press on the host is now the button's, and a second press closes the menu. The popover part checks it.
  • A number stayed on the toolbar icon after turning Anubis off on a page where it had removed panels: the page sent its count with hidden results zeroed but removed panels kept.
  • A site on two hand-written lines was ranked by its first line on search pages but shown with its last line's ranking in the menu, the popup, and Settings. They now all take the first (listSites, with a test).
  • Smaller things: when loading a page fails and the second try in a hidden frame fails too, the summary gives the first reason (too slow, failed) rather than "no results"; parsed pages get a <base> so their links resolve against the page they came from; an earlier copy's room made for pinned results is cleared; the popup keeps checking while more results load instead of once after 2.5 s; import errors in Settings are shown instead of lost, and the import line reads as a sentence ("Read as an Anubis list", not "a Anubis list").
  • Polish: the result menu, the hidden-result line, and the "from page 2" note took their words from code; they're now in messages.json, sharing the popup's words where they say the same thing. Lists of names in the menu, the popup, a rule's tags, and Settings' tag panels use "a, b, and c".

Show hidden after a new search, and tag choices from files (2026-09-29) ​

  • Found: Show hidden stayed on after a new search that Google or DuckDuckGo made without loading a page, so the next search opened with its hidden results showing, while results shown one at a time were already forgotten there. It now ends with the search, like those. The reveal e2e part presses Show hidden, starts a search with history.pushState, and checks nothing is revealed; it failed on the previous build.
  • Found: a backup or sync file's tag choices went into storage unchecked. A choice that wasn't an object (null) stopped search pages at collectTags, and a colour that wasn't one went into a result's style. Reading a file now keeps only what Settings saves: a known action, a #rgb or #rrggbb colour, a text label, and a true-or-false muted. Unit tests in tests/storage.test.ts failed before the change.

Result selector indicators (2026-09-29) ​

  • Removed redundant "Pinned" and "Hidden" chips; "Raised" and "Lowered" remain. The scale button's pin is gold and keeps its accessible ranking name. DuckDuckGo's light and dark mocks check that other selector icons match the neighbouring menu button, while hover remains gold.

Screen readers and focus on search pages (2026-09-29) ​

Four of the gaps ACCESSIBILITY.md listed, fixed on search pages. The a11y e2e part reads the accessibility tree (CDP's Accessibility.getFullAXTree, which sees into closed shadow roots) and checks each; the previous build fails all of them.

  • Each ⇅ button names its site: "Hide, rank or tag javascript.info", as a label and a tooltip, instead of nine buttons all called "Hide, rank or tag this site". The hidden line's Show button is labelled "Show mythology.fandom.com" (it still reads "Show"; the label starts with the visible word, so voice control finds it), and moves focus to the result it brought back, since the line goes with the click.
  • The change line is announced. "Pinned javascript.info." goes into a role="status" region that stays in the summary's shadow root; one created along with its text isn't reliably read.
  • The summary keeps focus. Its buttons carry data-focus-key, and render in ui.ts focuses the same key after replacing the content, or the first button when that one is gone (Undo, Show all). Show hidden keeps focus on itself, now "Hide them again".
  • × in the result menu puts focus back on the ⇅ button, as Escape did.
  • Reading the focused element: the tree marks the page itself as focused too, so the check takes the last focused node that isn't the page.

Keyboard shortcuts ​

  • Two commands: Alt+Shift+O turns Anubis on or off, Alt+Shift+H shows hidden results and hides them again. They need no permission. On a Mac they use Control, because Option+Shift types characters (Ø, Ó) and would be taken from text fields.
  • Show hidden lives in the content script's state, so the background script sends the tab a toggle-reveal message rather than changing storage.
  • A test can't press a browser-level shortcut, so the e2e shortcuts part checks the keys are registered and sends the same message the background script does.
  • The popup's tooltips show the keys the browser actually assigned (commands.getAll()), since people can change them or another extension can claim them first.

Phone layouts (Firefox for Android) ​

  • Found: Google sends phones a different layout, where titles are div role="heading" aria-level="3" rather than h3 (uBlacklist's "Web (mobile)" rules). Anubis found no results at all there: the new mobile e2e part, against a googleMobile mock, reported 0 results on the previous build and 7 now.
  • Shipped: an engine can carry mobile changes, chosen from the user agent (Mobi, as engines do; tablets get the computer layout) when the content script starts. Google's heading selector adds ARIA headings but leaves out top stories cards ([data-news-cluster-id]), which have the same headings. Load more results is off on Google phones until the phone layout's paging is known.
  • Found in the phone screenshot: a long hidden line ran its Show button under the ⇅ button, and the summary sat against the screen edge. Hidden lines now leave room for the button, the summary gets an inset below 600 px, and the ⇅ button is 32 px on touch screens.
  • Tried: emulating the phone with the DevTools protocol's device metrics. Full-page screenshots came out cropped and scrolled sideways; a plain 412 px viewport with a phone user agent is enough.
  • Not yet: gecko_android in the manifest. AMO offers an add-on on Android from the first version whose manifest has it, so it waits until a phone has been checked (ROADMAP.md).

Clean up pages ​

Asked for: a global option to force-remove AI answers (Gemini's AI Overview, Duck.ai) and other clutter such as video panels.

  • An AI answer came back after hiding a site (2026-09-30): reported on DuckDuckGo: the AI answer was removed, a site was hidden from its menu, and the answer was on the page again with the summary at its top. Where the summary can't sit above an AI answer (a grid), it goes at the top of the answer, and clean-up never removes a block that holds the summary. Once the summary had moved into a removed answer, the next pass left the answer in place. Clean-up now removes a block whose only piece of Anubis is the summary at its very top (summaryOnlyAtTop in cleanup.ts), and the summary never goes inside an answer that's removed. The Google aigrid mock reproduces it with Show hidden then Hide them again: before the fix, the answer stayed with the summary inside it. The cleanup part now checks that. The live DuckDuckGo markup wasn't available, so the exact layout there isn't confirmed.
  • Tried first, rejected: class-name selectors from community uBlock filter lists (for example .M8OgIe, .hdzaWe and [data-mcpr] for Google's AI Overview). They are the usual approach, but the lists warn that Google rotates these names, the problem the README describes for results.
  • Shipped: blocks are found from their visible heading ("AI Overview", "Videos", "People also ask"…, matched whole and ignoring case, with common translations in utils/cleanup.ts), then widened to the block in the results column. The column is the results' own list, the engine's boundary (#rso, role="main"…), or an element inside that boundary which also holds results. A heading that never reaches the column, like "Images" in Google's side panel, is left alone, and a block that contains a result or the search box is never removed. When the heading's block sits next to another heading of the same or higher level, only that section goes: "Images" inside a panel in the column removes the image row, not the panel. A first version of that rule stopped before checking the column and removed the side panel's image row too; the cleanup e2e part covers both. A few selectors from filter lists back the headings up where a heading isn't enough.
  • Forcing it: removing an element after the engine renders it can always be outrun by a redesign, so the switches that stop AI answers at the source are used too. "AI answers" sent DuckDuckGo searches to noai.duckduckgo.com, DuckDuckGo's own no-AI version, which has no Search Assist or Duck.ai (dropped later; see "DuckDuckGo stays put"). For Google, "Always open the Web tab" adds udm=14, Google's own filter for plain web links (the Web tab under More). It's a separate switch because it also drops everything else that isn't a link. Choosing All from the Web tab is remembered for that search in the tab's sessionStorage, so it isn't bounced straight back.
  • Tried the same day, then dropped: the redirect only ran on duckduckgo.com, so start. and safe. were sent too, with kp=1 (DuckDuckGo's documented strict safe search parameter) for safe., which noai. wouldn't otherwise know about.
  • DuckDuckGo stays put (2026-09-29): the redirect cost more than it saved. DuckDuckGo keeps its settings in cookies for duckduckgo.com only, so noai. reset region, theme, and safe search; every search loaded twice; and the address changed under people. DuckDuckGo documents URL parameters for its settings but none for AI (checked its settings/params.md), and carrying settings over would mean reading undocumented cookie names. So "AI answers" now removes DuckDuckGo's AI answer on the page, like Google's and Brave's: by its "Search Assist" label where there is one, and by EasyList's selectors (duckassist-answer-content, the wikinlp module). Duck.ai's tab, links, and search-box button go the way Google's AI Mode tab does, and like it aren't counted, since they aren't content (AI_ENTRY_POINTS in utils/cleanup.ts: whole-text, title, or label matches, plus EasyList's selectors). A tab's item in its row goes too when it holds little more than the tab. The ai=1 DuckDuckGo mock models all of it, and its check failed on the previous build, which left for noai.. noai.duckduckgo.com itself still works with Anubis for anyone who opens it.
  • Not used: a noai=1 URL parameter for DuckDuckGo, mentioned by one blog but not in DuckDuckGo's documented parameters (duckduckgo-help-pages/_docs/settings/params.md). Brave's summary=0 parameter, which Brave community threads report no longer works. A declarativeNetRequest rule that rewrites Google URLs before the page loads: it's faster, but it needs a new permission, and the redirect from the content script is quick enough.
  • Bug found in use: "AI answers" didn't remove Google's AI Overview. Only heading elements were read as labels, but the label can be a plain div beside an icon, and an icon's <title> adds text that breaks an exact match. The guard against removing the search box also refused any block with a q field, which a follow-up box inside the Overview may have. Now any short text node that is a label counts, as does text only an AI answer has ("AI responses may include mistakes"), selectors from community filter lists (.M8OgIe, .YzCcne) back them up, and only the page's first search box is protected. An aiLabel Google mock covers this; the previous build removes nothing on it. Google's real markup is still unconfirmed; the troubleshooting page asks for its structure (tags, classes, and roles, no text).
  • Bug found in use: "Videos" removed only the heading above a video panel. Each video has a title heading in a link, so Anubis took the videos for results, and clean-up refused to remove a block containing results. Worse, the sitelink rule merged the videos into one "result" (they share a site), which swallowed the panel's heading too. Now only the main list of results (the list holding the most) is protected, results inside a removed panel go with it and leave the summary's counts, and sitelinks are only merged under a result that shows its address, which videos don't. The aiLabel Google mock has such a panel; the previous build leaves it in place.
  • Still only the heading, on live Google: a screenshot showed the dashed "removed" outline around the "Videos" header row alone. Three layouts give that, and all three are now Google mocks (videos=titles|groups|split) that fail on the previous build: each video's title is a heading outside its link, which the section rule took for a separate section; the videos have <h3> links, and Google's results sit in small groups, so the videos were the biggest "list" and got protected as the main results; or the header row, the videos, and "View all" are separate blocks. Main results are now the ones that show an address; a heading repeated across look-alike cards is an item title, not a section; and when a removed block is little more than its label, the blocks after it go too, up to a result or another section. Which layout Google really uses is still unknown; the troubleshooting page's snippet now covers panels as well as AI answers.
  • Fixed from the live structure: on the real page Anubis stopped at the video panel's header row (div.UjLRDc) instead of the panel. Google marks each block in the list with data-rpos, so engines can now declare a blocks selector: a recognised heading inside a marked block removes the whole block, up to the column, unless a real section of it is meant ("Images" in a panel keeps the panel). A "section" that is only its heading row no longer counts as one, and clickable cards ([role=link], [jsaction]) count as items. The videos=google mock copies the reported structure; the previous build left the videos in place on it.
  • Visible and undoable: the summary names what was removed ("…and removed an AI answer and a video panel"), and "Show hidden" brings removed blocks back on that page, marked with a dashed outline. The AI Mode tab is removed but not counted, since it isn't content.

Lowered results and the button on DuckDuckGo (2026-09-30) ​

  • Pushed out of the result again (2026-09-30): reported from use with a screenshot of a pinned W3Schools result: the button sat outside the result's frame, right of the ⋯ menu, with nothing visible beside the menu. Two changes. Text that doesn't show (checkVisibility with opacity and visibility) no longer counts as covered; what DuckDuckGo keeps beside the menu is a guess (a see-through layer holding the menu's items), modelled by the unseen mock, where 7 of 7 buttons left the menu before the fix. And beside a menu, the button never leaves the result: if under the menu also covers text, it goes back beside the menu, since the engine cuts its text off before the menu anyway.

  • Lowered results no longer fade. They were at 58% opacity until hovered, which read as greyed out, as if they couldn't be used. A lowered site is still one you may want to read: it moves down, and its "Lowered" label says why. The Ranking sites guide and the homepage demo changed with it.

  • The button left the result on DuckDuckGo. It sits just left of each result's ⋯ menu, then moves away if it covers text: under the menu first, then past the result's right edge. DuckDuckGo cuts long addresses off with an ellipsis before the menu, but the cut-off text still reports its full width, so the button counted as covering it. It ended up under the menu on some results and outside the result (and outside a pinned result's frame) on others. Only text inside its clipping boxes counts now (clipOf in ui.ts). The DuckDuckGo mock's addresses are now long, cut-off breadcrumbs, as on the live page. The pages part asserts every button sits beside its menu: 3 of 7 didn't before the fix.

Hidden results: removed by default ​

Feedback from use, with a screenshot of a page where one site filled most results: the "Collapse" style's line per hidden result ("fandom.com hidden by your list · Show", fifteen times) clogged the page.

  • Default changed from Collapse to Remove. Hidden results leave the page; the summary counts them ("Anubis hid 12 of 20 results") and Show hidden brings them back, so nothing goes without a trace. Settings saved with the old default move over once (sync:hideStyleMoved records it); choosing Collapse afterwards sticks.
  • Collapse, better: hidden results in a row now share one line ("starwars.fandom.com and 1 more hidden by your list"), whose Show brings back the whole run. A run is results that are next to each other in the page, skipping Anubis's own elements and removed panels. The runs e2e part checks it on a Google page where one site is everywhere.
  • The e2e harness keeps Collapse (most checks use the lines) and marks the migration as done, since it would otherwise switch the seeded setting to Remove the moment the extension installs.

Reranking ​

  • Tried: moving result nodes in the DOM. Rejected before building: DuckDuckGo and Google render results with their own scripts (React on DuckDuckGo), which own those nodes; moving them risks breaking "More results", keyboard navigation, and hydration.
  • Shipped: the results' parent becomes a flex column and each result gets a CSS order. Nothing moves in the DOM. Caveats: vertical margins no longer collapse between results (slightly larger gaps on some layouts), and keyboard navigation (DuckDuckGo's j/k) follows DOM order, not visual order.
  • Bug found: ties between a boosted result and its neighbour went to the original order, so boost=1 never moved anything. Ties now go to the higher score.
  • Bug found (2026-09-29): pinned frames crossed on Google. A pinned result's frame is an outline 7px outside it, chosen because it moves nothing. On a live Google page with several reddit.com results pinned, the frames of neighbouring pins crossed, and the last one cut through the next result. The flex column is the cause: Google spaces results with a margin inside each one, which collapses through the result in Google's own layout but stays inside it once the result is a flex item, so the boxes touch. The inner Google mock models it (the pins e2e part: gaps of 0 on the previous build). Fixed: after reranking, makeRoomForPins measures the space between each pinned result and what's drawn next to it, and gives the lower one a larger margin-top until there's room for each frame (8px) plus 8px between: 24px between two pinned results, 16px between a pinned result and any other. It measures rather than adding a fixed margin, so layouts that already have room (the DuckDuckGo mock, 26px) don't move. In a flex column the margin needed doesn't depend on the margin already set, so it doesn't creep from pass to pass. Also tried, on paper: one frame around a run of pins, which needs a partial outline that CSS can't draw, or an element of Anubis's own inside each result, which an overflow: hidden result would clip.

Load more results (more than one page of results) ​

Called "Weigh deeper" until the wording review below.

  • Brave's pager between pages, Bing loading nothing, and saying why (2026-09-30): reported from use. On Brave, the Next button sat between page 1 and the loaded pages, and the page count stopped at 2 while it seemed to keep loading. On Bing, pressing Load more results loaded nothing. Loaded results were appended to the end of the results' parent, and the engine's pager is the last item there (Brave's is reported so; Bing's li.b_pag is the last item of #b_results); they now go right after the last result, and reranking keeps whatever follows the last result below every result, however far one is lowered. For Bing, the live check below already found the fetched copy of page 2 was a robot check with status 200, which reads as a page with no results. A page without results is now tried once more in a hidden frame (anubis-frame, sandboxed without top navigation), where the engine's scripts run as on a page you opened, so a check that passes by itself leads on to the results. In Firefox, requests go through content.fetch, which MDN documents as sending them as the page would: a content script's own fetch is the extension's there. Each request and each frame has a time limit, so loading can't hang, and a 429 or 503 gets one retry after its Retry-After (at most 10 s). The summary now says why a load stopped (no results, the same results again, slow down, refused, too slow, failed) with Open page N; before, the button just vanished. Why Brave stopped at page 2 isn't known: the mock loads pages 2 and 3 on the old build too, so it was something only the live page does (a hang, a 429, or a robot check are the candidates, and each now shows in the summary). The deeper part now loads two Brave pages with the pager inside the results list (the pager was above page 2 before the fix), loads a Bing page whose request gets a robot check but whose page load doesn't (0 pages before, 1 after), and checks the summary's line when both get it. Not yet confirmed on live pages, including whether Bing's check passes in a frame.

  • Counting the engine's own pages (2026-09-30): pressing DuckDuckGo's own "More results" didn't count as a page, so the summary said one page fewer and Load more results started its count from the wrong page. Anubis now notes a press of the engine's button that isn't its own, and counts a page once the new results arrive. The deeper part presses the mock's button and expects 2 pages; before the fix it had 1.

  • A typed amount instead of a dropdown (2026-09-30): the project owner asked to choose their own number of pages, with a limit only if there is one. Anubis has none of its own, but engines do: most stop sending pages or show a robot check well before 20, and each page adds about a second (a 700 ms pause plus the load). The dropdown is now a number from 0 to 20 (MAX_DEEPER, clampDeeper in utils/storage.ts), with a tip under it that changes as you type: off, a few seconds that engines rarely mind, a warning that some engines may ask for a robot check from 4 pages, and that Google and Bing often do from 10. The content script clamps the stored value too, so a backup or sync file can't ask for more.

  • The automatic setting loaded nothing on Google (2026-09-29): reported from use. The button worked, but "5 more pages" never loaded any. The content script starts at document_start and Google streams its page, so the first pass found results before the pager (a#pnnext) at the foot of the page had arrived. The automatic load found no Next link, took that for the last page, and gave up for the search, which also took the button away. Pressing the button later worked because by then the page had finished. Anubis now waits for the Next link (or DuckDuckGo's button, as it already did) before it offers or starts loading (nextPageReady in deeper.ts). A Google mock whose pager arrives 600 ms after the page (latePager) loaded nothing before the fix. The deeper part now asserts its results and runs with checks, so CI covers it. Not yet confirmed on the live page, and not checked: whether Google rewriting its address after loading starts the automatic load a second time (it would fetch page 2 again, find only duplicates, and stop).

  • Engines only send one page, so an extension can only rerank what is on screen. Weigh deeper brings the next pages onto the current one: DuckDuckGo gets its own "More results" button pressed; Google, Bing, and Yahoo have their next-page link fetched; Brave and Ecosia get their page parameter incremented. Fetched pages are parsed with DOMParser and run through the same result finder as the live page, so no per-engine import code was needed.

  • Off by default and never automatic unless chosen, with 700 ms between page requests, because extra requests to an engine can trigger rate limits or CAPTCHAs. Requests only go to the engine's own origin.

  • Bug found: the automatic mode ran during the first pass, before the mutation observer's state existed (a temporal dead zone error in the minified build). The observer is now set up before the first pass.

  • The automatic setting (2026-09-29) was a row of three underlined words, Off, +1 page, and +2 pages, which read poorly as a choice and stopped at two. It is now a dropdown from Off to "5 more pages", six pages in all (MAX_DEEPER). A stepper was considered, but a dropdown is the control settings already use for picking one value from a short list.

Shadow DOM UI ​

  • All UI on search pages lives in closed shadow roots, so the engine's CSS can't restyle it and its scripts can't read your tag names. They can still see the data-anubis-* attributes on results, as with uBlacklist.
  • Not tried on purpose: adoptedStyleSheets for sharing one stylesheet across roots. It has been unreliable from Firefox content scripts (Xray wrappers), so each root gets a <style> element; the CSS is small.
  • Bug found: re-rendering looked up the previous content with root.querySelector(':scope > …'). Inside a shadow root :scope matches nothing, so old content was never removed and summaries and the weigh menu piled up after changes. Each host now remembers the node it rendered.
  • Bug found: the weigh menu focused the first weight (Hide) instead of the chosen one, because a comma selector returns the first match in document order.
  • Bug found: after a change the menu re-positioned to its result, which reranking had just moved, so it jumped off-screen. It now stays where it opened.
  • web-ext lint flagged innerHTML for the constant SVG icons. Icons are now parsed with DOMParser and cached; the Firefox build lints clean.

Undo (2026-09-29) ​

A change from the result menu shows in the summary above the results, "Hid fandom.com." with Undo, in keeping with nothing being hidden without a trace. With the Remove style the result disappears under the menu, and pressing the ranking again doesn't undo it: it clears your ranking, which for a raised site you then pinned means Normal, not Raise.

  • What it restores: the site's line in your list as it was (ranking and tags), not the whole list, so a change made meanwhile in settings or another tab survives. A tag defined by the change goes too, unless another site uses it.
  • Changes in a row to one site merge: Hide then Lower undoes to how the site was before Hide. Back where it started, the line goes away. A change to another site replaces it; there's one level of undo.
  • When it goes: at the next search, or when the site changes elsewhere (checked when the list reloads; the site still reading as before the change means the reload is from before it, not that it's stale).
  • Where: a line of its own in the summary, under the sentence and above the tags, in the text colour rather than muted, since it's the one thing on the line you just caused. It needs the summary: with the summary turned off, or on DuckDuckGo Lite, which has none, there's no Undo; the menu still shows the site's ranking.
  • Wording: past-tense verbs, as in the summary sentence ("Hid", "Lowered", "Raised", "Pinned"). Pressing the ranking a site already had says "Cleared your ranking of fandom.com." rather than "back to normal", since a list may still rank it. Several changes to one site say "Changed fandom.com in your list."

Filtering by tag ​

  • Shipped: the summary lists the tags on the page's visible results with their counts, a legend you can click to show only one tag ("Showing only “Discussion”: 2 of 9 results"). It's the lightest version of Kagi's lenses: nothing is fetched, and a new search clears it. Filtered-out results get data-anubis-filtered and display: none, so they keep their place for when the filter is lifted.

Released under the GNU AGPL v3.