system HF Staff commited on
Commit
fb3fe52
ยท
verified ยท
1 Parent(s): c5cc3db

Sync from felladrin/MiniSearch@a00aebb

Browse files
.env.example CHANGED
@@ -37,3 +37,6 @@ SEARCH_FALLBACK_PARALLEL_API_KEY=""
37
 
38
  # The API key for You.com. Optional; without it the keyless "profile=free" endpoint is used, which allows 100 queries per day.
39
  SEARCH_FALLBACK_YOUCOM_API_KEY=""
 
 
 
 
37
 
38
  # The API key for You.com. Optional; without it the keyless "profile=free" endpoint is used, which allows 100 queries per day.
39
  SEARCH_FALLBACK_YOUCOM_API_KEY=""
40
+
41
+ # Whether a search that asks for an official page may run a second, "site:"-scoped query to reach the official site itself. Off by default, since it sends the engines a second request; only "true" or "1" turn it on.
42
+ SEARCH_OFFICIAL_LANE_ENABLED="false"
.github/workflows/publish-docker-image.yml CHANGED
@@ -147,7 +147,7 @@ jobs:
147
  id: record-commit
148
  run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
149
  - name: Log in to the Container registry
150
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
151
  with:
152
  registry: ${{ env.REGISTRY }}
153
  username: ${{ github.actor }}
 
147
  id: record-commit
148
  run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
149
  - name: Log in to the Container registry
150
+ uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4
151
  with:
152
  registry: ${{ env.REGISTRY }}
153
  username: ${{ github.actor }}
changelog.md CHANGED
@@ -3,6 +3,14 @@
3
  Notable changes, grouped by date, most recent first.
4
 
5
 
 
 
 
 
 
 
 
 
6
  ## 2026-11-03
7
 
8
  - The five thinking models added on 2026-11-02 (Spark X2.6 1.7B and 4B, Granite 4.2 3B, Qwen 3.5 4B, Gemma 4 E2B) no longer ship with thinking switched off. Their chain of thought arrives on wllama's `reasoning_content` channel and is now wrapped in the configured reasoning markers, so the existing thinking UI shows it while the model reasons and the answer streams in after. The model warmup counts a reasoning token as a working model, so thinking models no longer log a failed warmup.
 
3
  Notable changes, grouped by date, most recent first.
4
 
5
 
6
+ ## 2026-11-05
7
+
8
+ - Relative month and year words in a query are anchored to absolute ones before the search leaves the browser: "next month" reaches the engines as "December 2026" and "ๆ˜Žๅนด" as "2027ๅนด". The anchor is appended rather than substituted, so a query that already names a year keeps its own phrasing, and two anchors per query is the ceiling. The typed query still drives the prompt, the UI, and the history entry; only the copy sent to the search engines changes, and the search cache key changes with it. Anchors skip titles and phrases that are not dates ("ๆ˜ŽๅนดไปŠๆ—ฅ", "็™ปไธŠๆœˆ็ƒ", "the last year of college") and leave "recent" and "latest" alone, because recency is what the engines already rank for. Day words ("tomorrow", "ๆ˜Žๅคฉ") stay as typed: their anchor would be an ISO date, which pages rarely write, and many queries use them as names ("usa today"). Only English and Chinese phrases are recognized. `shared/relativeDates.ts` holds the rules and `client/modules/textGeneration.ts` applies them.
9
+ - The page reader drops structural navigation before it extracts text: `nav`, `aside`, `header`, `footer` and `form` blocks go, and so does any block whose text is mostly links (60% or more, in blocks of 120 characters and up), judged innermost-first so a page-wide wrapper cannot take the article down with it. Inside an `<article>`, a `<main>` or a `role="main"` block, link density removes nothing, because a list of linked titles there is usually the answer itself. The page's title and h1/h2 words also join the passage score at 0.15 weight, so a page whose heading names the query breaks ties against its own navigation. On the sortiraparis December page the extracted text fell from 57,160 to 2,128 characters and the answering passage moved from 10th of 61 to 3rd of 8, which is what puts it inside a page's excerpt share. A link-list home page now contributes far less body text, and the reader accepts that trade: a thin excerpt beats one made of navigation links.
10
+ - A result from the site the query names gets a bonus on its semantic score before the score filter: +0.5 when a query word equals the domain's label ("disney" and disney.com, subdomains included), +0.75 when the label is the query's own words joined ("disney plus" and disneyplus.com, "coca cola" and coca-cola.com). The bonus is added rather than multiplied because the reranker returns a raw logit that is usually negative, where a factor above one would demote the named site instead of promoting it. Substrings do not count (disneyfan.com gets nothing), filler words never match on their own (plus.com, news.com), and a query that names a year will not promote a page that states a different year, so a brand's recap of last year's event cannot displace fresher reporting; a page that states no year at all keeps the bonus, because relative-time queries are anchored to a year before they reach the search. A label that spells only the start of the query leaves words unused and gets nothing (`tesla investor relations` and teslainvestor.com), a label that starts with a filler word is skipped (`the disney store` and thedisney.com), a split label counts from four characters, and a trailing dot on the hostname changes nothing. The site's name comes from the public suffix list, private section included (the `tldts` package), so bbc.co.uk answers "bbc" and a tenant of a shared host such as someone.github.io answers its own name, not "github". The pinned top result is never boosted, so the score spread the client reads relevance tags from keeps its shape. The reranker still never sees the URL, and a result the search did not return cannot be added here: the boost reorders and re-selects what the pipeline already holds, which is enough to move a page into the six the model reads. `server/authorityRanking.ts` holds the rule and `docs/reranking.md` records it.
11
+ - A text search that asks for an official page can now reach the official site even when the engine's results never included it, behind a new setting, `SEARCH_OFFICIAL_LANE_ENABLED`, which is off by default because it sends the engines a second request. When it is on, the query carries navigation intent (`ๅฎ˜็ฝ‘`, `ๆŽ’็‰‡`, `่ฅไธšๆ—ถ้—ด`, `official`, `hours`, `showtimes` and the like, in Chinese and English only), and one of the returned results names a site, the server sends a second, `site:`-scoped query to SearXNG and merges up to three of its results, deduplicated by URL and kept to that registrable domain, behind the pinned first result: the ranking re-sorts only what follows the top result, so a page appended past that window would never be re-sorted at all. The domain has to come from the results themselves, by the same rule the score bonus uses, because guessing `<token>.com` from the query would turn a search box into a scanner; a query that names a brand whose official site is absent from every result gets no second query. The second query is one request with no retry, and its failures count toward neither the SearXNG circuit nor the engine health in `/status`, so it cannot slow down or degrade the searches users are waiting on. SearXNG still sees the extra request, so an engine that suspends the instance after a CAPTCHA does so for every search. It runs only when SearXNG served the search (never after the fallback), never uses the last five seconds of the search deadline, and is canceled at eight seconds; a failure only logs and is counted, so it can add results but never turn a served search into a 502. `/status` reports `searches.officialLane` (searches, `resultsAdded`, `failures`), and `server/officialLane.ts` holds the rule.
12
+
13
+
14
  ## 2026-11-03
15
 
16
  - The five thinking models added on 2026-11-02 (Spark X2.6 1.7B and 4B, Granite 4.2 3B, Qwen 3.5 4B, Gemma 4 E2B) no longer ship with thinking switched off. Their chain of thought arrives on wllama's `reasoning_content` channel and is now wrapped in the configured reasoning markers, so the existing thinking UI shows it while the model reasons and the answer streams in after. The model warmup counts a reasoning token as a working model, so thinking models no longer log a failed warmup.
client/modules/textGeneration.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import gptTokenizer from "gpt-tokenizer";
2
  import prettyMilliseconds from "pretty-ms";
3
  import {
@@ -447,8 +448,19 @@ async function startTextSearch(query: string, invocation: number) {
447
  let pageContentsRead = Promise.resolve();
448
  let textSearchFailed = false;
449
 
450
- const searchQuery =
451
- query.length > 2000 ? (await getKeywords(query, 20)).join(" ") : query;
 
 
 
 
 
 
 
 
 
 
 
452
 
453
  if (getSettings().enableImageSearch) {
454
  updateImageSearchState("running");
@@ -466,7 +478,9 @@ async function startTextSearch(query: string, invocation: number) {
466
  if (textResults.length === 0) {
467
  const queryKeywords = await getKeywords(query, 10);
468
  const keywordOutcome = await searchText(
469
- queryKeywords.join(" "),
 
 
470
  getSettings().searchResultsLimit,
471
  );
472
  textResults = keywordOutcome.results;
 
1
+ import { normalizeRelativeDates } from "@shared/relativeDates";
2
  import gptTokenizer from "gpt-tokenizer";
3
  import prettyMilliseconds from "pretty-ms";
4
  import {
 
448
  let pageContentsRead = Promise.resolve();
449
  let textSearchFailed = false;
450
 
451
+ // "ๆ˜Žๅนด" or "next month" name no year to the search engine, which then
452
+ // matches undated pages and leaves the answer to guess. Only this machine's
453
+ // clock knows which year the user means, so the query sent to the engine
454
+ // carries the anchor; the question, the history, and the prompt keep the
455
+ // wording the user typed.
456
+ const asOf = new Date();
457
+
458
+ const searchQuery = normalizeRelativeDates(
459
+ query.length > 2000 ? (await getKeywords(query, 20)).join(" ") : query,
460
+ asOf,
461
+ // The server rejects a longer query outright, so the anchor has to fit.
462
+ { maxLength: 2000 },
463
+ ).searchQuery;
464
 
465
  if (getSettings().enableImageSearch) {
466
  updateImageSearchState("running");
 
478
  if (textResults.length === 0) {
479
  const queryKeywords = await getKeywords(query, 10);
480
  const keywordOutcome = await searchText(
481
+ normalizeRelativeDates(queryKeywords.join(" "), asOf, {
482
+ maxLength: 2000,
483
+ }).searchQuery,
484
  getSettings().searchResultsLimit,
485
  );
486
  textResults = keywordOutcome.results;
docs/configuration.md CHANGED
@@ -108,6 +108,14 @@ Each provider has its own terms, and each operator is responsible for following
108
 
109
  That is why a provider that answers HTTP 429 is paused: the cascade skips it, without a network call, until its `Retry-After` has passed (60 seconds when the header is missing or invalid, and never more than an hour or less than a second). A public instance should set a key for each provider.
110
 
 
 
 
 
 
 
 
 
111
  ## Application Settings
112
 
113
  Settings are stored in browser localStorage and can be changed via the Settings UI.
 
108
 
109
  That is why a provider that answers HTTP 429 is paused: the cascade skips it, without a network call, until its `Retry-After` has passed (60 seconds when the header is missing or invalid, and never more than an hour or less than a second). A public instance should set a key for each provider.
110
 
111
+ ### Official Page Lane
112
+
113
+ A search that asks for an official page can miss the official site: an engine answering "disney ไธ‹ไธชๆœˆๆŽ’็‰‡" with aggregators and fan blogs leaves the schedule page out of the set entirely, and no ranking of that set can recover it. With the lane on, when the query carries navigation intent and one of the returned results names a site, the server asks SearXNG a second, `site:`-scoped question and merges the answers before ranking. `docs/reranking.md` records the rule; this variable governs it.
114
+
115
+ | Variable | Default | Description |
116
+ | ------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `SEARCH_OFFICIAL_LANE_ENABLED` | `false` | Run the second, `site:`-scoped search when the query asks for an official page. Only `true` or `1` turns it on, and changing it takes a restart, since a process keeps the environment it was started with. It is off by default because it sends the engines a second request, which an instance they already rate-limit cannot afford. The lane runs only when SearXNG served the search (never after the fallback), never uses the last five seconds of the search deadline, and is canceled at eight seconds. It is one request with no retry, and its failures count toward neither the SearXNG circuit nor the engine health in `/status`: a failure only logs and is counted under `searches.officialLane`, never turning a served search into a 502. SearXNG still sees the extra request, so an engine that suspends the instance after a CAPTCHA does so for every search |
118
+
119
  ## Application Settings
120
 
121
  Settings are stored in browser localStorage and can be changed via the Settings UI.
docs/overview.md CHANGED
@@ -89,13 +89,14 @@ The system executes two parallel flows when a user submits a query:
89
  ### Search Flow
90
 
91
  1. User submits a query via SearchForm
92
- 2. Client checks IndexedDB cache for matching query hash
93
- 3. On cache miss: authenticated HTTP request to `/search/text` or `/search/images`
94
- 4. Server verifies request token via `searchToken.ts` (CSRF protection)
95
- 5. `webSearchService.ts` forwards query to SearXNG at `http://127.0.0.1:8888`
96
- 6. Raw results are deduplicated, cleaned, and optionally reranked
97
- 7. Image results keep the thumbnail URLs SearXNG returned, and results that came back without one are dropped; the client loads each tile on its own from `/thumbnail`, so the response does not wait on any thumbnail host
98
- 8. Results returned as structured JSON and cached in IndexedDB (15-minute freshness TTL)
 
99
 
100
  ### AI Generation Flow
101
 
 
89
  ### Search Flow
90
 
91
  1. User submits a query via SearchForm
92
+ 2. Relative month and year words (English and Chinese for now) are anchored to absolute ones before the request (`shared/relativeDates.ts`), so "next month" reaches the engines as "December 2026" and a relative phrase cannot leave the index without a year to match; the query as typed still drives the prompt, the history entry, and the UI
93
+ 3. Client checks IndexedDB cache for matching query hash
94
+ 4. On cache miss: authenticated HTTP request to `/search/text` or `/search/images`
95
+ 5. Server verifies request token via `searchToken.ts` (CSRF protection)
96
+ 6. `webSearchService.ts` forwards query to SearXNG at `http://127.0.0.1:8888`
97
+ 7. Raw results are deduplicated, cleaned, and optionally reranked; a result from the site the query names gets a small score boost (`server/authorityRanking.ts`, see `docs/reranking.md`)
98
+ 8. Image results keep the thumbnail URLs SearXNG returned, and results that came back without one are dropped; the client loads each tile on its own from `/thumbnail`, so the response does not wait on any thumbnail host
99
+ 9. Results returned as structured JSON and cached in IndexedDB (15-minute freshness TTL)
100
 
101
  ### AI Generation Flow
102
 
docs/page-content.md CHANGED
@@ -72,19 +72,34 @@ read degrades one result at a time rather than the whole answer.
72
  ## Extraction
73
 
74
  `server/pageContentService.ts` converts HTML with `html-to-text`, preferring the
75
- innermost `article` / `main` / `[role="main"]` container and skipping nav,
76
- header, footer, aside, forms and inline widgets. Readability-style extraction
 
77
  via `@mozilla/readability` would score better on hostile layouts, but it needs a
78
  full DOM implementation at runtime; `html-to-text` is already a dependency and
79
  the passage ranking absorbs most of what boilerplate removal would have.
80
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  Encoding comes from the `Content-Type` header, falling back to the `<meta
82
  charset>` the document declares, so a page that is not UTF-8 does not reach the
83
  model as mojibake.
84
 
85
  Passages are ranked by how much of the query they cover, with a prior for lead
86
  passages (the definition or summary usually opens a page). The prior is worth at
87
- most half a matched term, so it settles ties without outweighing coverage.
 
 
88
 
89
  For pools of at most 256 passages, the server also scores every passage with
90
  its local multilingual bi-encoder. Lexical and dense ranks are combined with
 
72
  ## Extraction
73
 
74
  `server/pageContentService.ts` converts HTML with `html-to-text`, preferring the
75
+ innermost `article` / `main` / `[role="main"]` container and skipping inline
76
+ widgets; nav, header, footer, aside and forms are dropped by the scan described
77
+ below. Readability-style extraction
78
  via `@mozilla/readability` would score better on hostile layouts, but it needs a
79
  full DOM implementation at runtime; `html-to-text` is already a dependency and
80
  the passage ranking absorbs most of what boilerplate removal would have.
81
 
82
+ Sites built out of plain `div`s put the same chrome where no selector names it,
83
+ so a single forward scan (`stripLinkHeavyBlocks`) runs before the conversion. It
84
+ drops `nav`, `aside`, `header`, `footer` and `form` blocks, and any block of 120
85
+ characters or more whose text is 60% or more link text, judged innermost-first
86
+ so that a wrapper which also holds the article survives. Inside `article`,
87
+ `main` or `role="main"`, link density drops nothing: a list of linked titles
88
+ there is usually the answer. The scan stays linear in the page size, which
89
+ matters because it runs on the main thread for any public URL: an unclosed
90
+ `<script>` runs to the end of the page, as a parser reads it, and a closing tag
91
+ with no open block to match skips the walk down the stack. A page that reads as
92
+ empty after the scan falls back to its unstripped text.
93
+
94
  Encoding comes from the `Content-Type` header, falling back to the `<meta
95
  charset>` the document declares, so a page that is not UTF-8 does not reach the
96
  model as mojibake.
97
 
98
  Passages are ranked by how much of the query they cover, with a prior for lead
99
  passages (the definition or summary usually opens a page). The prior is worth at
100
+ most half a matched term, so it settles ties without outweighing coverage. A
101
+ second prior, worth 0.15, rewards a passage for repeating the page's own
102
+ `<title>` and `h1`/`h2` words, which a rail of unrelated teasers does not do.
103
 
104
  For pools of at most 256 passages, the server also scores every passage with
105
  its local multilingual bi-encoder. Lexical and dense ranks are combined with
docs/reranking.md CHANGED
@@ -97,16 +97,57 @@ When `preserveTopResults = true`, the ranking algorithm:
97
 
98
  This mode ensures the original top result is never displaced by the reranker, while still improving the ordering of subsequent results.
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  ## Integration with Search Pipeline
101
 
102
  The search endpoint coordinates reranking in `searchEndpointServerHook.ts`:
103
 
104
  ```
105
  1. fetchSearXNG(query, searchType, limit) - internally fetches from SearXNG, deduplicates, and cleans results
106
- 2. Check getRerankerStatus()
 
107
  - If healthy: rankSearchResults(query, searchType, results, preserveTopResults)
108
  - If unhealthy: return unranked results (fallback)
109
- 3. Return structured JSON response
110
  ```
111
 
112
  Reranking is applied to both text and image search results. For image results, titles and URLs are reformatted into the same text-tuple shape used for text search before being sent through the reranker, then the original image data (including thumbnails) is re-matched to the reranked order by URL. Text search passes `preserveTopResults: true` to keep the original top SearXNG result pinned, while image search does not.
 
97
 
98
  This mode ensures the original top result is never displaced by the reranker, while still improving the ordering of subsequent results.
99
 
100
+ ### Domain Priority
101
+
102
+ A page from the site the query names gets a small bonus on its semantic score before the filter runs (`server/authorityRanking.ts`, applied in `server/rankSearchResults.ts`). The reranker never sees the URL, so a query like "disney plus next month" ranks a blog roundup and disneyplus.com on their prose alone; the bonus lets the named site overtake a page whose snippet merely talks about it.
103
+
104
+ The bonus is added, not multiplied, because the reranker returns a raw cross-encoder logit: most documents score below zero, and a factor above one would push a negative score further down and demote the very site it means to promote.
105
+
106
+ The rule is narrow enough to explain in a sentence:
107
+
108
+ | Situation | Bonus | Example |
109
+ | ------------------------------------------- | ----- | -------------------------------------- |
110
+ | A query word equals the domain's label | +0.5 | `disney` โ†’ disney.com, `wwe` โ†’ wwe.com |
111
+ | The label is the query's words joined | +0.75 | `disney plus` โ†’ disneyplus.com |
112
+ | The label is the query's words with hyphens | +0.75 | `coca cola` โ†’ coca-cola.com |
113
+ | The label merely contains a query word | 0 | `disney` โ†’ disneyfan.com |
114
+
115
+ Subdomains share their registrable domain's label, so investor.disney.com and www.disney.com both answer "disney". The label comes from the public suffix list, private section included (`tldts`), so bbc.co.uk answers "bbc", ministry.go.jp answers "ministry", and a tenant of a shared host such as someone.github.io answers its own name instead of "github". "+" is read as " plus ", because a hostname cannot contain it and a user typing "disney+" means the same brand.
116
+
117
+ Three guards keep the nudge from promoting the wrong page:
118
+
119
+ - Filler words never match on their own, so "plus size guide" does not promote plus.com and "news about disney" does not promote news.com. A label split into the query's words counts only when one part is four characters or more, unless a hyphen marks the split.
120
+ - The joined-label bonus needs the words the query is actually about: a label that spells only the start of the query leaves the remaining words unused, so "tesla investor relations" does not promote teslainvestor.com, and a label that starts with a filler word is skipped, so "the disney store" does not promote thedisney.com.
121
+ - When the query names a year, a page that states a different year is left alone: a brand's recap of last year's event is not the page the query meant, and fresher reporting should keep the top spot. A page that states no year at all keeps the bonus, because relative-time queries are anchored to a year before they reach the search ("next month December 2026") and the page that answers them often writes only "December".
122
+
123
+ The filler-word list is English. In another language, the other words of the query count as leftover, so the joined-label bonus rarely applies there, while the single-word bonus still does.
124
+
125
+ The bonus is applied before the standard-deviation filter, so it can also rescue a page the filter would have dropped. It cannot add a result the search did not return; that is a retrieval problem, not a ranking one. The pinned top result of a search is never boosted, so the score distribution the client reads relevance tags from keeps its shape.
126
+
127
+ ### Official Page Lane
128
+
129
+ A bonus can reorder what a search returned, but it cannot summon a page the search never returned. When a query asks for an official page and the engine's results contain only aggregators and fan pages, there is nothing to promote, so the endpoint asks for the page directly: a second SearXNG query scoped to `site:<domain>`, whose results are merged into the set before ranking (`server/officialLane.ts`, wired in `server/searchEndpointServerHook.ts`).
130
+
131
+ The lane is off by default, because it sends the engines a second request; `SEARCH_OFFICIAL_LANE_ENABLED=true` turns it on. When it is on, two conditions have to hold, because a scoped second query costs latency and must not guess:
132
+
133
+ - The query carries navigation intent. Chinese has no word boundaries to split on, so the phrases are matched in the raw query (`ๅฎ˜็ฝ‘`, `ๅฎ˜ๆ–น็ฝ‘็ซ™`, `ๅฎ˜ๆ–น็ซ™`, `ๅฎ˜ๆ–น็ฝ‘้กต`, `ๅฎ˜ๆ–น้กต้ข`, `็‰‡ๅ•`, `ๆŽ’็‰‡`, `ๅœบๆฌก`, `็ฅจไปท`, `่ฅไธšๆ—ถ้—ด`, `ไธŠๆ˜ ๆ—ถ้—ด`, `่ดญ็ฅจ`, `่ฎข็ฅจ`); English matches whole words (`official`, `hours`, `opening`, `schedule`, `showtime`, `showtimes`, `timetable`, `ticket`, `tickets`, `pricing`, `prices`). Other languages have no intent words yet, so their queries get no lane. The site has to appear in the query in the letters of its domain: "่ฟชๅฃซๅฐผ ๅฎ˜็ฝ‘" does not reach disney.com, while "disney ๅฎ˜็ฝ‘" does. Words that name the intent do not count as leftover for the domain match, so "disney plus showtimes" reaches disneyplus.com.
134
+ - One of the returned results already names the site, by the same rule the bonus uses (`namedDomain`). The domain is evidence from the search itself; no domain is derived from the query alone, since probing `<token>.com` would turn a search box into a scanner. A query that names a brand whose official site did not appear anywhere in the results gets no lane. The rule matches names, not ownership: for "disney world ticket prices", a third-party disneyworld.eu answers "disney world", while Disney's own disneyworld.disney.go.com answers "go", so the lane scopes the third-party site.
135
+
136
+ The lane adds at most three results, deduplicated by URL and kept to the registrable domain it scoped to, so an engine that ignores `site:` cannot smuggle an unrelated host in. They are placed behind the pinned first result: the ranking keeps the top result where it is and re-sorts only what follows, so a page appended past that window would never be re-sorted at all.
137
+
138
+ Every failure is the search it already has. The lane runs only when SearXNG served the search, since results from the fallback mean SearXNG has just failed. It never uses the last five seconds of the 25-second search deadline, and its request is canceled at eight seconds. The request goes through `fetchSupplementarySearXNG` (`server/webSearchService.ts`): one request with no retry, which counts toward neither the circuit breaker nor the engine-health counters, so a `site:` query that comes back empty cannot slow down, open the circuit for, or mark degraded the searches users are waiting on. SearXNG itself still sees the request, so an engine that suspends the instance after a CAPTCHA does so for every search. A failure or a timeout only logs and increments a counter: it can add results, never turn a served search into a 502. `/status` reports `searches.officialLane` with how often the lane ran, how many results it added, and how often it failed.
139
+
140
  ## Integration with Search Pipeline
141
 
142
  The search endpoint coordinates reranking in `searchEndpointServerHook.ts`:
143
 
144
  ```
145
  1. fetchSearXNG(query, searchType, limit) - internally fetches from SearXNG, deduplicates, and cleans results
146
+ 2. Text search only, when `SEARCH_OFFICIAL_LANE_ENABLED` is on: the official page lane may merge results from a `site:`-scoped second query (see above)
147
+ 3. Check getRerankerStatus()
148
  - If healthy: rankSearchResults(query, searchType, results, preserveTopResults)
149
  - If unhealthy: return unranked results (fallback)
150
+ 4. Return structured JSON response
151
  ```
152
 
153
  Reranking is applied to both text and image search results. For image results, titles and URLs are reformatted into the same text-tuple shape used for text search before being sent through the reranker, then the original image data (including thumbnails) is re-matched to the reranked order by URL. Text search passes `preserveTopResults: true` to keep the original top SearXNG result pinned, while image search does not.
package-lock.json CHANGED
@@ -56,6 +56,7 @@
56
  "shiki": "^4.0.0",
57
  "temp-dir": "^3.0.0",
58
  "throttleit": "^3.0.0",
 
59
  "vite": "^8.0.0",
60
  "wouter": "^3.3.0",
61
  "yet-another-react-lightbox": "^3.21.6",
@@ -95,9 +96,9 @@
95
  "license": "MIT"
96
  },
97
  "node_modules/@ai-sdk/gateway": {
98
- "version": "4.0.132",
99
- "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.132.tgz",
100
- "integrity": "sha512-ppStDEz96g+JEuxGiVTP29OKBjPyYZXRxs38wnq3427U2QbZtxe6TukrgSAAq0n4yaHvSvVAWJY9kBlW8XudYg==",
101
  "license": "Apache-2.0",
102
  "dependencies": {
103
  "@ai-sdk/provider": "4.0.35",
@@ -2149,29 +2150,29 @@
2149
  }
2150
  },
2151
  "node_modules/@shikijs/core": {
2152
- "version": "4.5.0",
2153
- "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz",
2154
- "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==",
2155
  "license": "MIT",
2156
  "dependencies": {
2157
- "@shikijs/primitive": "4.5.0",
2158
- "@shikijs/types": "4.5.0",
2159
- "@shikijs/vscode-textmate": "^10.0.2",
2160
  "@types/hast": "^3.0.5",
2161
- "hast-util-to-html": "^9.0.5"
2162
  },
2163
  "engines": {
2164
  "node": ">=20"
2165
  }
2166
  },
2167
  "node_modules/@shikijs/engine-javascript": {
2168
- "version": "4.5.0",
2169
- "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz",
2170
- "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==",
2171
  "license": "MIT",
2172
  "dependencies": {
2173
- "@shikijs/types": "4.5.0",
2174
- "@shikijs/vscode-textmate": "^10.0.2",
2175
  "oniguruma-to-es": "^4.3.6"
2176
  },
2177
  "engines": {
@@ -2179,38 +2180,38 @@
2179
  }
2180
  },
2181
  "node_modules/@shikijs/engine-oniguruma": {
2182
- "version": "4.5.0",
2183
- "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz",
2184
- "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==",
2185
  "license": "MIT",
2186
  "dependencies": {
2187
- "@shikijs/types": "4.5.0",
2188
- "@shikijs/vscode-textmate": "^10.0.2"
2189
  },
2190
  "engines": {
2191
  "node": ">=20"
2192
  }
2193
  },
2194
  "node_modules/@shikijs/langs": {
2195
- "version": "4.5.0",
2196
- "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz",
2197
- "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==",
2198
  "license": "MIT",
2199
  "dependencies": {
2200
- "@shikijs/types": "4.5.0"
2201
  },
2202
  "engines": {
2203
  "node": ">=20"
2204
  }
2205
  },
2206
  "node_modules/@shikijs/primitive": {
2207
- "version": "4.5.0",
2208
- "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz",
2209
- "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==",
2210
  "license": "MIT",
2211
  "dependencies": {
2212
- "@shikijs/types": "4.5.0",
2213
- "@shikijs/vscode-textmate": "^10.0.2",
2214
  "@types/hast": "^3.0.5"
2215
  },
2216
  "engines": {
@@ -2218,24 +2219,24 @@
2218
  }
2219
  },
2220
  "node_modules/@shikijs/themes": {
2221
- "version": "4.5.0",
2222
- "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz",
2223
- "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==",
2224
  "license": "MIT",
2225
  "dependencies": {
2226
- "@shikijs/types": "4.5.0"
2227
  },
2228
  "engines": {
2229
  "node": ">=20"
2230
  }
2231
  },
2232
  "node_modules/@shikijs/types": {
2233
- "version": "4.5.0",
2234
- "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz",
2235
- "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==",
2236
  "license": "MIT",
2237
  "dependencies": {
2238
- "@shikijs/vscode-textmate": "^10.0.2",
2239
  "@types/hast": "^3.0.5"
2240
  },
2241
  "engines": {
@@ -2243,9 +2244,9 @@
2243
  }
2244
  },
2245
  "node_modules/@shikijs/vscode-textmate": {
2246
- "version": "10.0.2",
2247
- "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
2248
- "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
2249
  "license": "MIT"
2250
  },
2251
  "node_modules/@sindresorhus/is": {
@@ -2884,9 +2885,9 @@
2884
  }
2885
  },
2886
  "node_modules/@vitejs/plugin-react": {
2887
- "version": "6.1.2",
2888
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz",
2889
- "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==",
2890
  "license": "MIT",
2891
  "dependencies": {
2892
  "@rolldown/pluginutils": "^1.0.1"
@@ -2895,9 +2896,9 @@
2895
  "node": "^20.19.0 || >=22.12.0"
2896
  },
2897
  "peerDependencies": {
2898
- "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
2899
  "babel-plugin-react-compiler": "^1.0.0",
2900
- "oxc-transform-react": "^0.152.0",
2901
  "vite": "^8.0.0"
2902
  },
2903
  "peerDependenciesMeta": {
@@ -3024,12 +3025,12 @@
3024
  }
3025
  },
3026
  "node_modules/ai": {
3027
- "version": "7.0.159",
3028
- "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.159.tgz",
3029
- "integrity": "sha512-4e/BwDoKJlMNYZaqERcTbGte+4TwroN9qKnCprog/XVfD9InzVPlhCviDhN452JIRrRsYg9BT0WsQ2C3Ds2mVQ==",
3030
  "license": "Apache-2.0",
3031
  "dependencies": {
3032
- "@ai-sdk/gateway": "4.0.132",
3033
  "@ai-sdk/provider": "4.0.35",
3034
  "@ai-sdk/provider-utils": "5.0.72"
3035
  },
@@ -3970,9 +3971,9 @@
3970
  }
3971
  },
3972
  "node_modules/globals": {
3973
- "version": "17.13.0",
3974
- "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
3975
- "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
3976
  "dev": true,
3977
  "license": "MIT",
3978
  "engines": {
@@ -4062,9 +4063,9 @@
4062
  }
4063
  },
4064
  "node_modules/hast-util-to-html": {
4065
- "version": "9.0.5",
4066
- "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
4067
- "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
4068
  "license": "MIT",
4069
  "dependencies": {
4070
  "@types/hast": "^3.0.0",
@@ -7213,18 +7214,18 @@
7213
  }
7214
  },
7215
  "node_modules/shiki": {
7216
- "version": "4.5.0",
7217
- "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz",
7218
- "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==",
7219
  "license": "MIT",
7220
  "dependencies": {
7221
- "@shikijs/core": "4.5.0",
7222
- "@shikijs/engine-javascript": "4.5.0",
7223
- "@shikijs/engine-oniguruma": "4.5.0",
7224
- "@shikijs/langs": "4.5.0",
7225
- "@shikijs/themes": "4.5.0",
7226
- "@shikijs/types": "4.5.0",
7227
- "@shikijs/vscode-textmate": "^10.0.2",
7228
  "@types/hast": "^3.0.5"
7229
  },
7230
  "engines": {
@@ -7520,7 +7521,6 @@
7520
  "version": "7.0.25",
7521
  "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
7522
  "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==",
7523
- "dev": true,
7524
  "license": "MIT",
7525
  "dependencies": {
7526
  "tldts-core": "^7.0.25"
@@ -7533,7 +7533,6 @@
7533
  "version": "7.0.25",
7534
  "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz",
7535
  "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==",
7536
- "dev": true,
7537
  "license": "MIT"
7538
  },
7539
  "node_modules/tough-cookie": {
 
56
  "shiki": "^4.0.0",
57
  "temp-dir": "^3.0.0",
58
  "throttleit": "^3.0.0",
59
+ "tldts": "^7.0.25",
60
  "vite": "^8.0.0",
61
  "wouter": "^3.3.0",
62
  "yet-another-react-lightbox": "^3.21.6",
 
96
  "license": "MIT"
97
  },
98
  "node_modules/@ai-sdk/gateway": {
99
+ "version": "4.0.133",
100
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.133.tgz",
101
+ "integrity": "sha512-6DmtrnwvTZG6RFhZscsnUX0XKxA+o2Y/EkZUGWWqFITez/gvOGG/PRKITjwPupUci5vbORThwqkbCj1/066Reg==",
102
  "license": "Apache-2.0",
103
  "dependencies": {
104
  "@ai-sdk/provider": "4.0.35",
 
2150
  }
2151
  },
2152
  "node_modules/@shikijs/core": {
2153
+ "version": "4.6.0",
2154
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.6.0.tgz",
2155
+ "integrity": "sha512-bfd4OKgvoyXYiYmMRuleA656P4V7M/Du9lcTl38Ngm6KszMj7bq4J1IGZwSYR4c95Tv5UQALtYNWUwaI9UT3xw==",
2156
  "license": "MIT",
2157
  "dependencies": {
2158
+ "@shikijs/primitive": "4.6.0",
2159
+ "@shikijs/types": "4.6.0",
2160
+ "@shikijs/vscode-textmate": "^10.1.0",
2161
  "@types/hast": "^3.0.5",
2162
+ "hast-util-to-html": "^9.0.6"
2163
  },
2164
  "engines": {
2165
  "node": ">=20"
2166
  }
2167
  },
2168
  "node_modules/@shikijs/engine-javascript": {
2169
+ "version": "4.6.0",
2170
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.6.0.tgz",
2171
+ "integrity": "sha512-K0oqxFoL+kEMdz89mHLFO15Tsbv3kFZ6WR1cXZKMvLSc+oqaM00A6pyga1qSeXB22dP0tTiSApRcGBaKWsEccg==",
2172
  "license": "MIT",
2173
  "dependencies": {
2174
+ "@shikijs/types": "4.6.0",
2175
+ "@shikijs/vscode-textmate": "^10.1.0",
2176
  "oniguruma-to-es": "^4.3.6"
2177
  },
2178
  "engines": {
 
2180
  }
2181
  },
2182
  "node_modules/@shikijs/engine-oniguruma": {
2183
+ "version": "4.6.0",
2184
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.6.0.tgz",
2185
+ "integrity": "sha512-aggd6Q7SuJOXNnsUbfG2XesxBJKBMAw/PDTUo9PyRjMaaJJ2dNrI+q0C+25ARXAK9yLHAlwxc9oRT2u68YKZ7w==",
2186
  "license": "MIT",
2187
  "dependencies": {
2188
+ "@shikijs/types": "4.6.0",
2189
+ "@shikijs/vscode-textmate": "^10.1.0"
2190
  },
2191
  "engines": {
2192
  "node": ">=20"
2193
  }
2194
  },
2195
  "node_modules/@shikijs/langs": {
2196
+ "version": "4.6.0",
2197
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.6.0.tgz",
2198
+ "integrity": "sha512-1yMEX3oCVV54LUq/ovfpRkDQoYDkcvjSWFFa/YLUDMiSoRzMi2uxLJYbv41ahT8Lu9lFMaa7hecd3Q39trqUyg==",
2199
  "license": "MIT",
2200
  "dependencies": {
2201
+ "@shikijs/types": "4.6.0"
2202
  },
2203
  "engines": {
2204
  "node": ">=20"
2205
  }
2206
  },
2207
  "node_modules/@shikijs/primitive": {
2208
+ "version": "4.6.0",
2209
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.6.0.tgz",
2210
+ "integrity": "sha512-c54+TQEo9ToqgfL/AU8FpjnV09+GA9Oa7M5ROkkXteZfp5XzpAJGXjjvZQFQioJOwm/w/6Z3lIzQstAkl8coag==",
2211
  "license": "MIT",
2212
  "dependencies": {
2213
+ "@shikijs/types": "4.6.0",
2214
+ "@shikijs/vscode-textmate": "^10.1.0",
2215
  "@types/hast": "^3.0.5"
2216
  },
2217
  "engines": {
 
2219
  }
2220
  },
2221
  "node_modules/@shikijs/themes": {
2222
+ "version": "4.6.0",
2223
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.6.0.tgz",
2224
+ "integrity": "sha512-qHAVAs80RiGD+IRuH9G+JPDOEc9A/kHsng4EzCUCwB9bLzW6Z0y9WETh2YyCcADJBmfrIaJ1Jp7KBNP6FHkgtQ==",
2225
  "license": "MIT",
2226
  "dependencies": {
2227
+ "@shikijs/types": "4.6.0"
2228
  },
2229
  "engines": {
2230
  "node": ">=20"
2231
  }
2232
  },
2233
  "node_modules/@shikijs/types": {
2234
+ "version": "4.6.0",
2235
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.6.0.tgz",
2236
+ "integrity": "sha512-+ENG1adsKt/X7cRYw1Nl7BEpBmPOjmQ7bR/eGmHuMlT8gtOFf3KEi5l31DJYyAWhW6Y6bjP1plU7FmaxWGENFw==",
2237
  "license": "MIT",
2238
  "dependencies": {
2239
+ "@shikijs/vscode-textmate": "^10.1.0",
2240
  "@types/hast": "^3.0.5"
2241
  },
2242
  "engines": {
 
2244
  }
2245
  },
2246
  "node_modules/@shikijs/vscode-textmate": {
2247
+ "version": "10.1.0",
2248
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz",
2249
+ "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==",
2250
  "license": "MIT"
2251
  },
2252
  "node_modules/@sindresorhus/is": {
 
2885
  }
2886
  },
2887
  "node_modules/@vitejs/plugin-react": {
2888
+ "version": "6.2.1",
2889
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.2.1.tgz",
2890
+ "integrity": "sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==",
2891
  "license": "MIT",
2892
  "dependencies": {
2893
  "@rolldown/pluginutils": "^1.0.1"
 
2896
  "node": "^20.19.0 || >=22.12.0"
2897
  },
2898
  "peerDependencies": {
2899
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0 || ^0.3.0",
2900
  "babel-plugin-react-compiler": "^1.0.0",
2901
+ "oxc-transform-react": ">=0.152.0 <0.157.0",
2902
  "vite": "^8.0.0"
2903
  },
2904
  "peerDependenciesMeta": {
 
3025
  }
3026
  },
3027
  "node_modules/ai": {
3028
+ "version": "7.0.160",
3029
+ "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.160.tgz",
3030
+ "integrity": "sha512-h48o8i8NTDuHUZhiLDm9NHufEk/L3dkrJ8E9liXMcd4K4hAdI8bfamtbRJylyQeKQ599mnWNplTvyH/SbtpolQ==",
3031
  "license": "Apache-2.0",
3032
  "dependencies": {
3033
+ "@ai-sdk/gateway": "4.0.133",
3034
  "@ai-sdk/provider": "4.0.35",
3035
  "@ai-sdk/provider-utils": "5.0.72"
3036
  },
 
3971
  }
3972
  },
3973
  "node_modules/globals": {
3974
+ "version": "17.14.0",
3975
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz",
3976
+ "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==",
3977
  "dev": true,
3978
  "license": "MIT",
3979
  "engines": {
 
4063
  }
4064
  },
4065
  "node_modules/hast-util-to-html": {
4066
+ "version": "9.0.6",
4067
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.6.tgz",
4068
+ "integrity": "sha512-1SZUxCN3TtJAE7e2BQN2MBHsoZ1gnmi9GoCP5Di59CSWptIezSAW09yDZyVbanb78V9eCwrKqdQsjtfVTxYHWA==",
4069
  "license": "MIT",
4070
  "dependencies": {
4071
  "@types/hast": "^3.0.0",
 
7214
  }
7215
  },
7216
  "node_modules/shiki": {
7217
+ "version": "4.6.0",
7218
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.6.0.tgz",
7219
+ "integrity": "sha512-imjLsVHgPuWhvpU3P+zOekvMYqnel7QmKsFC4Sn0fCuAvaaalBsxwKEsrU8Ko5BdFnZA6Go/8a0RzahS634UgA==",
7220
  "license": "MIT",
7221
  "dependencies": {
7222
+ "@shikijs/core": "4.6.0",
7223
+ "@shikijs/engine-javascript": "4.6.0",
7224
+ "@shikijs/engine-oniguruma": "4.6.0",
7225
+ "@shikijs/langs": "4.6.0",
7226
+ "@shikijs/themes": "4.6.0",
7227
+ "@shikijs/types": "4.6.0",
7228
+ "@shikijs/vscode-textmate": "^10.1.0",
7229
  "@types/hast": "^3.0.5"
7230
  },
7231
  "engines": {
 
7521
  "version": "7.0.25",
7522
  "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
7523
  "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==",
 
7524
  "license": "MIT",
7525
  "dependencies": {
7526
  "tldts-core": "^7.0.25"
 
7533
  "version": "7.0.25",
7534
  "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz",
7535
  "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==",
 
7536
  "license": "MIT"
7537
  },
7538
  "node_modules/tough-cookie": {
package.json CHANGED
@@ -88,6 +88,7 @@
88
  "shiki": "^4.0.0",
89
  "temp-dir": "^3.0.0",
90
  "throttleit": "^3.0.0",
 
91
  "vite": "^8.0.0",
92
  "wouter": "^3.3.0",
93
  "yet-another-react-lightbox": "^3.21.6",
 
88
  "shiki": "^4.0.0",
89
  "temp-dir": "^3.0.0",
90
  "throttleit": "^3.0.0",
91
+ "tldts": "^7.0.25",
92
  "vite": "^8.0.0",
93
  "wouter": "^3.3.0",
94
  "yet-another-react-lightbox": "^3.21.6",