Degoog Docs

Degoog Plugins

Create custom bang commands, slots, search result tabs, search bar actions, routes, and middleware.

What plugins are

Your plugins belong in the data/plugins/ directory, or whatever you set DEGOOG_PLUGINS_DIR to. A plugin is simply a folder containing an entry file. You can use a single plugin to handle multiple tasks like a bang command, a slot, search bar actions, HTTP routes, or request middleware.

Folder structure

data/plugins/
          my-plugin/
            index.js        
            template.html   
            style.css       
            script.js       
            card.html       
            author.json     

Your entry file needs to be index.js, index.ts, index.mjs, or index.cjs.

Asset files

  • template.html: This is the HTML fragment for your plugin output. You can use {{placeholders}} and replace them inside execute(). This gets injected directly into the search results rather than rendering a full page.
  • style.css: This loads automatically when your plugin is active. Make sure to use class names scoped to your specific plugin to avoid styling conflicts. You can check the Styling page for available CSS variables.
  • script.js: Your client side JavaScript that loads automatically.

You can add other files and read them while the app runs using ctx.readFile("filename") inside your init(ctx) function.

Plugin context and init()

The system calls your optional init(ctx) function once during startup before running configure(). The context provides the following:

init(ctx) {
          // ctx.template contains your template.html
          // ctx.dir is the absolute path to your plugin folder
          // ctx.readFile is an async function to read any file from your plugin folder
        }

For example, you might build a plugin that uses both a command template and a separate card template. The RSS plugin does exactly this by reading card.html through ctx.readFile("card.html").

Bang commands

Bang commands run whenever you type !trigger something into the search bar. You need to export a single object using export default or export const command containing these properties:

  • name and description: These appear in Settings and the !help menu.
  • trigger: The word you type right after the ! symbol. For example, typing weather becomes !weather.
  • execute(args, context) (async): The args parameter contains whatever text follows your trigger. You must return an object containing a title string and an html string. You can also include an optional totalPages number.

The second argument passed to execute is context, which might contain your clientIp and page.

You can also include optional properties like aliases for extra triggers, or naturalLanguagePhrases which are phrases that trigger the command without needing the ! symbol when natural language is enabled in Settings. You can also use settingsSchema, configure(settings), init(ctx), and isConfigured(). If isConfigured() is async and returns false, your command stays hidden from the !help menu until the user configures it.

For your settingsSchema, each field requires a key, label, and type. The type can be text, password, url, toggle, textarea, select, or urllist. You can also add optional flags like required, placeholder, description, or secret. Secret fields are never sent back to the browser. If you use the select type, you will need to add an options array containing your string choices.

How settings work

  1. Declare your settingsSchema so a Configure button appears in Settings then Plugins.
  2. When you save, the values are stored in data/plugin-settings.json under plugin-<folderName>.
  3. The configure(settings) function runs right after saving and every time the server restarts if settings are present.
  4. Use isConfigured() to return false when required settings are missing. This hides the command from the !help menu.
  5. You can disable any plugin using the toggle in Settings then Plugins. Disabled plugins disappear from !help and return an error if you try to invoke them.

Examples from the official store:

  • Weather is a bang command complete with a template, styles, settings for things like default city or Fahrenheit, aliases, and natural language phrases.
  • Define, QR, Time, and Password are examples of simple command only plugins.
  • Jellyfin and Meilisearch let you search your own personal backend through a bang command.

Slot plugins

Slots let you inject custom panels directly into the search results page when a query matches. Just export slot or slotPlugin. A single module can actually export both a slot and a bang command.

  • id and name: Your unique identifier and display name.
  • position: Choose between above-results, below-results, above-sidebar, below-sidebar, knowledge-panel, or at-a-glance. Please use the specific sidebar positions instead of the old deprecated sidebar option.
  • trigger(query): This needs to return true, or a Promise that resolves to true, whenever the slot should appear for a specific query.
  • execute(query, context) (async): Return an object with an optional title string and an html string. If you return an empty string for the html, nothing will show up. The context contains the clientIp and an array of results. Note that the results array is only populated if you set waitForResults: true on your plugin.

You can add an optional settingsId to specify the key where your settings are stored. This defaults to slot-<id>. You can also include settingsSchema, configure(settings), and init(ctx). If multiple slots match the exact same query, they will all display together.

Another optional flag is waitForResults. Set this to true if you need to receive context.results inside your execute() function. While results are always available when execution happens since slots run after the search finishes, they are only passed to your plugin if you explicitly enable this flag. It defaults to false so plugins that do not need results can stay completely independent.

You can also provide an optional slotPositions array. This lets you list multiple position values like ["knowledge-panel", "above-sidebar", "below-sidebar"]. If you include this, the application adds a Position dropdown in your plugin settings so you can choose exactly where the slot appears. If you leave it empty or omit it, the slot simply uses your fixed position value.

Sidebar positions

Using above-sidebar renders right at the top of the sidebar before any main content. Using below-sidebar renders at the very bottom of the sidebar below the engine timings and related searches.

Knowledge panel slot

Setting your position to knowledge-panel replaces only the very first block in the sidebar, which is usually the Wikipedia or knowledge panel. Your engine performance and related searches blocks stay exactly where they are. When your plugin returns content for this position, it shows up instead of the default knowledge panel.

At a glance slot

Slots using the at-a-glance position will fill the block directly above the search results. This does not delay your search response. The client shows your results immediately and fetches the panel content in a separate background request using POST /api/slots/glance with the query and results. Your execute(query, context) function will always receive context.results for these types of slots.

Examples from the official store:

  • TMDb is a slot above the results showing movie and TV details. It uses a secret API key stored in settings.
  • Math and GitHub are examples of slots appearing above the results.
  • RSS is a slot that reads feed URLs from settings and uses card.html through ctx.readFile().

Slot API: Calling POST /api/slots with the query and results returns the panels for the main positions. Calling POST /api/slots/glance returns only the glance panels. Slots also work perfectly for custom tabs. The client simply fetches the panels after a tab search and renders them in the exact same positions.

Search result tabs

Search result tabs let you add new entries to the main tab bar, just like the Web or Images tabs. Simply export tab or searchResultTab from your plugin folder. You use the exact same folder structure as any other plugin.

Required properties: You must provide an id and a name. You also need to provide either an engineType string like web, images, videos, news, or a custom type from your engines, or an executeSearch(query, page?, context?) async function. If you use the function, it must return an object containing your results array and an optional totalPages number. Each result needs a title, url, snippet, and source, plus an optional thumbnail and duration.

Optional properties: You can add an icon, settingsId, settingsSchema, configure(settings), and init(ctx). If you are sharing this on the Store, make sure to set your type to search-result-tab in your package.json. You can also add a dependencies array containing URLs if your tab relies on a specific engine, just like how the File tab depends on the Internet Archive engine.

API: A GET request to /api/search-tabs returns the list of available tabs. A GET request to /api/tab-search along with your tab id, query, and page number will run the specific search for that tab.

Search bar actions

Your plugins can also add buttons right next to the search bar. Just export searchBarActions as an array of objects containing an id, a label, and a type. The type can be navigate to open a specific URL, bang to fill the search bar with your trigger, or custom. If you use custom, your script.js will listen for a search-bar-action event that contains the action details. You can also include an optional icon image URL.

Plugin routes

Plugins can expose their own HTTP endpoints under the /api/plugin/<folderName>/ path. You do this by exporting routes as an array containing the method, path, and handler(req). Your method can be get, post, put, delete, or patch. The path is just the URL segment that comes after your plugin id. Your handler receives the standard Request object and needs to return a Response or a Promise that resolves to a Response. These routes become available the moment your plugin loads, meaning your script.js can call them immediately using fetch.

Request middleware

Middleware allows your plugin to hook into specific request flows. You export middleware as an object containing an id, a name, and a handle(req, context) function. The context might include specific route information like settings-auth. Your function should return a Response, a redirect URL object, or simply null to let the request continue normally. The application uses this middleware for the settings gate. To choose a plugin for the gate, just click Use as settings gate in that plugin configuration menu under Settings. This updates your plugin-settings.json file automatically.

Settings gate (login flow)

You can protect your settings using a simple password with DEGOOG_SETTINGS_PASSWORDS or by using a middleware plugin. When you use a plugin, anyone opening the Settings page goes through your custom flow like a magic link or external login before returning to the settings with a valid session token.

What your middleware must do

  • route "settings-auth": Return a JSON object with required set to true, valid set to false, and your loginUrl.
  • route "settings-auth-callback": After a successful authentication, return a redirect to /settings. The application will issue a session token and redirect the user automatically.
  • route "settings-auth-post": If you do not want to support password submissions via POST, just return a 400 error or something similar.

Make sure to expose a GET /login route that redirects back to the callback URL. You can clear the degoog-settings-token in your browser Session Storage anytime you need to test the flow.

Example: minimal bang command

Here is what a simple folder at data/plugins/greeting/ looks like with an index.js, template.html, and style.css file.

let template = "";
        
        export default {
          name: "Greeting",
          description: "Say hello",
          trigger: "hello",
          init(ctx) { template = ctx.template; },
          async execute(args) {
            const name = args.trim() || "world";
            const html = template.replace("{{name}}", name);
            return { title: "Hello", html };
          },
        };

template.html

<div class="command-result greeting">
          <h3 class="greeting-title">Hello, {{name}}!</h3>
        </div>

For your style.css, you should use variables like var(--text-primary) to ensure everything matches. You can find more details on the Styling page.

Debugging plugins

If you need to troubleshoot, set LOG_LEVEL=debug to print the execution times for all your plugin types directly to the server console. You can check the Environment variables page for more details.