Add opensearch.xml to site
This commit is contained in:
parent
f6b6639905
commit
3f3c0d72f8
@ -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
|
||||
|
@ -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
280
output.md
@ -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",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
```
|
14
package.json
14
package.json
@ -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
1904
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
public/opensearch.xml
Normal file
10
public/opensearch.xml
Normal 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>
|
33
scripts/generateOpenSearch.ts
Normal file
33
scripts/generateOpenSearch.ts
Normal 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!');
|
Loading…
x
Reference in New Issue
Block a user