A fast, local-first "redirection engine" for !bang users with a few extra features ^-^
1import { bangs } from "./bang";
2import "./global.css";
3
4function noSearchDefaultPageRender() {
5 const app = document.querySelector<HTMLDivElement>("#app")!;
6 app.innerHTML = `
7 <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh;">
8 <div class="content-container">
9 <h1>Und*ck</h1>
10 <p>DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables <a href="https://duckduckgo.com/bang.html" target="_blank">all of DuckDuckGo's bangs.</a></p>
11 <div class="url-container">
12 <input
13 type="text"
14 class="url-input"
15 value="https://unduck.link?q=%s"
16 readonly
17 />
18 <button class="copy-button">
19 <img src="/clipboard.svg" alt="Copy" />
20 </button>
21 </div>
22 </div>
23 <footer class="footer">
24 <a href="https://t3.chat" target="_blank">t3.chat</a>
25 •
26 <a href="https://x.com/theo" target="_blank">theo</a>
27 •
28 <a href="https://github.com/t3dotgg/unduck" target="_blank">github</a>
29 </footer>
30 </div>
31 `;
32
33 const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
34 const copyIcon = copyButton.querySelector("img")!;
35 const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
36
37 copyButton.addEventListener("click", async () => {
38 await navigator.clipboard.writeText(urlInput.value);
39 copyIcon.src = "/clipboard-check.svg";
40
41 setTimeout(() => {
42 copyIcon.src = "/clipboard.svg";
43 }, 2000);
44 });
45}
46
47const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? "g";
48const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG);
49
50function getBangredirectUrl() {
51 const url = new URL(window.location.href);
52 const query = url.searchParams.get("q")?.trim() ?? "";
53 if (!query) {
54 noSearchDefaultPageRender();
55 return null;
56 }
57
58 const match = query.match(/!(\S+)/i);
59
60 const bangCandidate = match?.[1]?.toLowerCase();
61 const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang;
62
63 // Remove the first bang from the query
64 const cleanQuery = query.replace(/!\S+\s*/i, "").trim();
65
66 // Format of the url is:
67 // https://www.google.com/search?q={{{s}}}
68 const searchUrl = selectedBang?.u.replace(
69 "{{{s}}}",
70 // Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
71 encodeURIComponent(cleanQuery).replace(/%2F/g, "/")
72 );
73 if (!searchUrl) return null;
74
75 return searchUrl;
76}
77
78function doRedirect() {
79 const searchUrl = getBangredirectUrl();
80 if (!searchUrl) return;
81 window.location.replace(searchUrl);
82}
83
84doRedirect();