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> 2526 <a href="https://x.com/theo" target="_blank">theo</a> 2728 <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();