Degoog Search engines
Build your own custom search backends for web, images, videos or anything you can think of.
Where engines live
You can drop your custom engines right into
data/engines/ or whatever you set
DEGOOG_ENGINES_DIR to. Each engine is just a file or
folder with an entry file that exports an object or class containing
a name and executeSearch.
Engine contract
What you need:
- name: The display name shown in Settings then Engines.
-
executeSearch(query, page?, timeFilter?, context)
(async): This needs to return a Promise that resolves to an array
of results. Each result is an object holding a
title,url,snippet,source, and optionally athumbnailandduration. Make sure you usecontext.fetchfor all your outbound HTTP requests so your engine respects any proxy settings the user has configured.
Optional extras:
-
bangShortcut: Lets users type a shortcut like
!shortcut queryto search only this engine (like settingbangShortcut: "ecosia"so users can type!ecosia linux). -
settingsSchema and
configure(settings): These work just like they do
for plugins. You will get a Configure button in Settings then
Engines, and the values are securely saved in
data/plugin-settings.json.
Your SettingField shape needs a key,
label, and type (which can be text,
password, url, toggle, textarea, select, or urllist). You can also
add optional properties like required,
placeholder, description,
secret, options (if using select), and
default.
If you have sensitive fields like API keys or tokens, mark them with
secret: true. The UI will never expose the saved value
to the user and will just show a Set or
Not set indicator instead. When saving, if the field
value equals the special "__SET__" string, the existing
stored value stays put. This ensures a page reload never wipes out a
key you already entered.
HTTP method
Your engines are completely free to use more than just GET requests.
Because you control the fetch call inside
executeSearch, you can use any method like POST and set
whatever headers or body your API needs. Here is what a GraphQL
engine using POST looks like:
async executeSearch(query, page = 1, _timeFilter, context) {
const doFetch = context?.fetch ?? fetch;
const response = await doFetch("https://graphql.example.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: MY_QUERY, variables: { search: query, page } }),
});
const data = await response.json();
// parse and return array of { title, url, snippet, source }
}
Search types
You can export a named type such as
"web" (which is the default), "images",
"videos", "news", or even your own custom
string like "books". Creating a custom type
automatically adds a new tab on the search results page. When
someone selects that tab, all engines sharing that type will run
together.
Language & time filters
When someone selects a language or time filter in the UI, those
values get passed to every engine through the
context argument. You do not have to read them if you
do not want to. Engines that ignore these will just return
unfiltered results.
| Field | Type | Description |
|---|---|---|
context.lang |
string | undefined |
The ISO 639 1 language code chosen by the user (like
"en", "de", or "it").
If it is undefined, no language filter was
selected.
|
context.buildAcceptLanguage() |
() => string |
Returns a ready to use Accept-Language header
value derived from context.lang. It safely falls
back to "en,en-US;q=0.9" when no language is set
so you can pass it directly to your fetch headers.
|
context.dateFrom |
string | undefined |
The start of a custom date range formatted as
YYYY-MM-DD. This is only set when the timeFilter
is custom.
|
context.dateTo |
string | undefined |
The end of a custom date range formatted as
YYYY-MM-DD. This is only set when the timeFilter
is custom.
|
context.extractImageUrl($el, baseUrl?, selectors?)
|
($el, baseUrl?, selectors?) => string
|
Easily extract an article image URL from a cheerio element. It
checks all the common attributes like src and
data src, normalizes URLs, and resolves relative
paths against your baseUrl. It even skips tiny
images and favicons. You can pass selectors to
target a specific thumbnail container first.
|
The timeFilter parameter passed to
executeSearch will be one of these options:
"any", "hour", "day",
"week", "month", "year", or
"custom". If it is "custom", just read
context.dateFrom and context.dateTo.
export default class MyEngine {
name = "My Search";
async executeSearch(query, page = 1, timeFilter, context) {
const lang = context?.lang;
const acceptLang = context?.buildAcceptLanguage?.() ?? "en,en-US;q=0.9";
const doFetch = context?.fetch ?? fetch;
const params = new URLSearchParams({ q: query });
if (lang) params.set("lang", lang);
if (timeFilter === "day") params.set("when", "24h");
else if (timeFilter === "week") params.set("when", "7d");
else if (timeFilter === "custom" && context?.dateFrom) {
params.set("from", context.dateFrom);
if (context.dateTo) params.set("to", context.dateTo);
}
const res = await doFetch(`https://example.com/search?${params}`, {
headers: { "Accept-Language": acceptLang },
});
// parse and return array of { title, url, snippet, source }
}
}
Safe Search
Our default engines like Brave, Google, and Bing include a
Safe Search dropdown in Settings then Engines. The
default behavior stays the same where Brave defaults to
moderate and the rest default to off.
Users can easily raise or lower the filter for each engine
individually.
You can add this exact same setting to your custom engines by
including a select field in your
settingsSchema and applying the value in your
executeSearch function:
settingsSchema = [
{
key: "safeSearch",
label: "Safe Search",
type: "select",
options: ["off", "moderate", "strict"],
description: "Filter explicit content from search results.",
},
];
safeSearch = "moderate";
configure(settings) {
if (typeof settings.safeSearch === "string") {
this.safeSearch = settings.safeSearch;
}
}
async executeSearch(query, page = 1, timeFilter, context) {
const params = new URLSearchParams({ q: query, safesearch: this.safeSearch });
// ...
}
Proxies
When someone enables a proxy for search, your engine requests can route through it. To make sure your engine plays nicely with proxies:
-
Use
context.fetchfor every outbound HTTP request instead of the globalfetch. -
Export outgoingHosts as an array of hostname
strings without the protocol or path. These hostnames get added to
the proxy allowlist. Only use
["*"]if your engine needs to fetch from random user provided URLs.
export const outgoingHosts = ["www.example.com", "example.com"];
export default class MyEngine {
name = "My Search";
async executeSearch(query, page = 1, _timeFilter, context) {
const url = `https://www.example.com/search?q=${encodeURIComponent(query)}`;
const doFetch = context?.fetch ?? fetch;
const response = await doFetch(url, { headers: { "User-Agent": "my-engine/1.0" } });
const html = await response.text();
// parse and return array of { title, url, snippet, source, ... }
}
}
Every engine also features an
Outgoing HTTP client control under the
Advanced section in Settings then Engines. This
setting is stored per engine and can be set to fetch,
system curl, auto, or any custom
transport you install from the Store.
You should still use context.fetch in your custom
engines so this setting applies automatically.
Setup
Create your data/engines/ directory or set your
DEGOOG_ENGINES_DIR. Add a single file like
my-engine.js or a folder with an
index.js file. The engine ID will simply be the
filename or folder name prefixed with engine-.
How settings work
-
Declare your
settingsSchemaso the Configure button appears in Settings then Engines. -
The user saves their preferences and the values are stored in
data/plugin-settings.json. -
Your
configure(settings)function is called right after saving and every time the server restarts. -
You can just return an empty array from
executeSearchif any required settings are missing.
Examples from the official store
-
Ecosia
is a web engine using
outgoingHostsandbangShortcutwhile correctly utilizingcontext.fetch. - Startpage is another web engine featuring optional Anonymous View settings.
- Internet Archive is a file type engine that works perfectly as a dependency for the File tab plugin.
If you plan on distributing your engine through the
Store, make sure to add a
screenshots/ folder inside your engine folder so the
Store card looks great.