Add opensearch.xml to site

This commit is contained in:
Gabriel Brown 2025-05-09 09:54:18 -05:00
parent f6b6639905
commit 3f3c0d72f8
7 changed files with 1074 additions and 1174 deletions

View File

@ -13,6 +13,7 @@ RUN pnpm install
COPY . .
# Build the project
RUN pnpm run prebuild
RUN pnpm run build
# Stage 2: Serve the app using the same version of Node

View File

@ -24,6 +24,12 @@
media="print"
onload="this.media='all'"
/>
<link
rel="search"
type="application/opensearchdescription+xml"
title="Bang!"
href="/opensearch.xml"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bang!</title>
<meta

280
output.md
View File

@ -1,280 +0,0 @@
docker/.env
```
PORT=5000
DOMAIN=bang.gbrown.org
NETWORK=node_apps
```
docker/Dockerfile
```
# Stage 1: Build the project
FROM node:18 AS builder
WORKDIR /app
# Copy package.json and pnpm-lock.yaml to the working directory
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN npm install -g pnpm
RUN pnpm install
# Copy project files into the docker image
COPY . .
# Build the project
RUN pnpm run build
# Stage 2: Serve the app using the same version of Node
FROM node:18-alpine
WORKDIR /app
# Install a simple http server
RUN npm install -g serve
# Copy built assets from the builder stage
COPY --from=builder /app/dist ./
# Expose port 5000 for the server
EXPOSE 5000
# Start the server using the `serve` package
CMD ["serve", "-s", ".", "-l", "5000"]
```
docker/docker-compose.yml
```yml
services:
bang-web-server:
build:
context: ../
dockerfile: docker/Dockerfile
container_name: bang
hostname: bang.gib
domainname: bang.gbrown.org
networks:
- nginx-bridge
#ports:
#- 5000:5000
tty: true
restart: unless-stopped
volumes:
- ../:/app
command: serve -s /app/dist -l 5000
networks:
nginx-bridge:
external: true
```
src/main.ts
```ts
import { bangs } from "./bang";
import "./global.css";
function noSearchDefaultPageRender() {
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHTML = `
<main class="main-container">
<div class="content-container">
<h1 class="bang-title">💣 Bang!</h1>
<p>
Add the following URL as a custom search engine to your browser in order to enable
<a href="https://duckduckgo.com/bang.html" target="_blank">
all of DuckDuckGo's bangs
</a>
right from your browser's search bar!
</p>
<div class="url-container">
<input
type="text"
class="url-input"
value="${import.meta.env.VITE_BANG_URL}?q=%s"
readonly
/>
<button class="copy-button">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<h3 class="settings-title">How to add Bang! Search Engine</h3>
<p style="margin-bottom: 8px; font-size: 14px;">
Below are links to the search engine settings in your browser.
<br />
Copy the URL for the browser engine you are using & paste it into a new tab.
<br />
From there you should see an option to add a search engine. Copy the link above
& fill out the form as follows.
</p>
<table class="form-table">
<tr>
<td><b>Name:</b></td>
<td>Bang!</td>
</tr>
<tr>
<td><b>Engine URL:</b></td>
<td>${import.meta.env.VITE_BANG_URL}?q=%s</td>
</tr>
<tr>
<td><b>Alias:</b></td>
<td>@bang</td>
</tr>
</table>
<div class="settings-links-container">
<div class="browser-link-container">
<img src="/firefox.svg" alt="Firefox" width="24" />
<input
type="text"
readonly value="about:preferences#search"
class="firefox-textbox"
title="Click to copy Firefox settings URL"
/>
<button class="copy-firefox">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<div class="browser-link-container">
<img src="/chrome.svg" alt="Chrome" width="24" />
<input
type="text"
readonly value="chrome://settings/searchEngines"
class="chrome-textbox"
title="Click to copy Chrome settings URL"
/>
<button class="copy-chrome">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
</div>
</div>
</main>
`;
const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
const copyIcon = copyButton.querySelector("img")!;
const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
const copyFirefox = app.querySelector<HTMLInputElement>(".copy-firefox")!;
const copyFirefoxIcon = copyFirefox.querySelector("img")!;
const firefoxInput = app.querySelector<HTMLInputElement>(".firefox-textbox")!;
const copyChrome = app.querySelector<HTMLInputElement>(".copy-chrome")!;
const copyChromeIcon = copyChrome.querySelector("img")!;
const chromeInput = app.querySelector<HTMLInputElement>(".chrome-textbox")!;
copyButton.addEventListener("click", async () => {
await navigator.clipboard.writeText(urlInput.value);
copyIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyIcon.src = "/clipboard.svg";
}, 2000);
});
copyFirefox.addEventListener("click", async () => {
await navigator.clipboard.writeText(firefoxInput.value);
copyFirefoxIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyFirefoxIcon.src = "/clipboard.svg";
}, 2000);
});
copyChrome.addEventListener("click", async () => {
await navigator.clipboard.writeText(chromeInput.value);
copyChromeIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyChromeIcon.src = "/clipboard.svg";
}, 2000);
});
}
const envDefaultBang = import.meta.env.VITE_DEFAULT_BANG ?? "g";
const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? envDefaultBang;
const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG);
function getBangredirectUrl() {
const url = new URL(window.location.href);
const query = url.searchParams.get("q")?.trim() ?? "";
if (!query) {
noSearchDefaultPageRender();
return null;
}
const match = query.match(/!(\S+)/i);
const bangCandidate = match?.[1]?.toLowerCase();
const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang;
// Remove the first bang from the query
const cleanQuery = query.replace(/!\S+\s*/i, "").trim();
// Format of the url is:
// https://www.google.com/search?q={{{s}}}
const searchUrl = selectedBang?.u.replace(
"{{{s}}}",
// Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
encodeURIComponent(cleanQuery).replace(/%2F/g, "/")
);
if (!searchUrl) return null;
return searchUrl;
}
function doRedirect() {
const searchUrl = getBangredirectUrl();
if (!searchUrl) return;
window.location.replace(searchUrl);
}
doRedirect();
```
src/vite-env.d.ts
```ts
/// <reference types="vite/client" />
```
tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
```
vite.config.ts
```ts
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
registerType: "autoUpdate",
}),
],
});
```

View File

@ -1,21 +1,25 @@
{
"name": "unduck",
"name": "bang",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"generate-opensearch": "ts-node scripts/generateOpenSearch.ts",
"prebuild": "pnpm generate-opensearch",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.1.0"
"@types/node": "^22.15.17",
"ts-node": "^10.9.2",
"typescript": "~5.7.3",
"vite": "^6.3.5"
},
"dependencies": {
"vite-plugin-pwa": "^0.21.1"
"vite-plugin-pwa": "^0.21.2"
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"

1904
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

10
public/opensearch.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Bang!</ShortName>
<Description>A better default search engine with bangs</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/svg+xml">https://bang.gbrown.org/bang.svg</Image>
<Url type="text/html" method="get" template="https://bang.gbrown.org?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="https://bang.gbrown.org/opensearch.xml"/>
<moz:SearchForm xmlns:moz="http://www.mozilla.org/2006/browser/search/">https://bang.gbrown.org</moz:SearchForm>
</OpenSearchDescription>

View File

@ -0,0 +1,33 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
// Get the current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get the Bang URL from environment or use a default for local development
const bangUrl = process.env.VITE_BANG_URL || 'https://bang.gbrown.org';
// Create the OpenSearch XML content
const openSearchXml = `<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Bang!</ShortName>
<Description>A better default search engine with bangs</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/svg+xml">${bangUrl}/bang.svg</Image>
<Url type="text/html" method="get" template="${bangUrl}?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="${bangUrl}/opensearch.xml"/>
<moz:SearchForm xmlns:moz="http://www.mozilla.org/2006/browser/search/">${bangUrl}</moz:SearchForm>
</OpenSearchDescription>`;
// Ensure the public directory exists
const publicDir = path.resolve(__dirname, '../public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
// Write the OpenSearch XML file
fs.writeFileSync(path.join(publicDir, 'opensearch.xml'), openSearchXml);
console.log('OpenSearch XML file generated successfully!');