Degoog Docs

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 a thumbnail and duration. Make sure you use context.fetch for 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 query to search only this engine (like setting bangShortcut: "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:

  1. Use context.fetch for every outbound HTTP request instead of the global fetch.
  2. 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

  1. Declare your settingsSchema so the Configure button appears in Settings then Engines.
  2. The user saves their preferences and the values are stored in data/plugin-settings.json.
  3. Your configure(settings) function is called right after saving and every time the server restarts.
  4. You can just return an empty array from executeSearch if any required settings are missing.

Examples from the official store

  • Ecosia is a web engine using outgoingHosts and bangShortcut while correctly utilizing context.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.